JWT-Shiro 整合
JWT-與Shiro整合進行授權認證的大致思路 圖示
大致思路
- 將登錄驗證從shiro中分離,自己結合JWT實現
- 用戶登陸后請求認證服務器進行密碼等身份信息確認,確認成功后 封裝相關用戶信息 生成token 相應給前端.
- 之后每次訪問資源接口都在請求頭中攜帶認證時生成的token
- 當發起資源請求時首先請求被請求過濾器攔截,攔截后判斷請求頭中是否含有token
- 如果含有token對token進行認證認證成功后對token進行解析,之后進行授權,擁有權限則進行放行
- 反之返回相關錯誤信息
核心點
- token相關工具類的封裝
- 自定義重寫shiro過濾器
extends AccessControlFilter
- 自定義實現shiro Realm
extends AuthorizingRealm
- 實現自定義的shiroToken
implements AuthenticationToken
具體代碼
生成token的工具類
public class JWTUtils {
//密鑰用於生成token的簽名
private static final String SIGN = "!1qaz.(";
/**
* 生成token
*/
public static String getToken(String userId,String userName, String roles, String permissions) {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE, 7);
JWTCreator.Builder builder = JWT.create()
.withIssuer("HuangShen")//token簽發者
.withExpiresAt(instance.getTime()) //過期時間
.withClaim("userId", userId)//相關信息
.withClaim("userName", userName)
.withClaim("roles", roles)
.withClaim("permissions", permissions);
//使用HMAC256算法生成token
String token = builder.sign(Algorithm.HMAC256(SIGN));
return token;
}
/**
* 解碼token
*
* @param token token
*/
public static DecodedJWT verify(String token){
DecodedJWT verify =JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
return verify;
}
自定義重寫shiro過濾器
public class JWTFilter extends AccessControlFilter {
/**
* 此方法首先執行當此方法返回false時繼續執行onAccessDenied方法
* 返回true允許訪問
* 返回 false拒絕訪問
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
//獲取主體對象
Subject subject = SecurityUtils.getSubject();
System.out.println("===允許訪問===");
//當主體對象不為空且已經獲得認證時允許訪問
if (null != subject && subject.isAuthenticated()){
return true;
}
return false;
}
/**
* 當isAccessAllowed返回值為false時執行
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String token = httpServletRequest.getHeader("token");
//客戶端沒有攜帶token
if (StringUtils.isEmpty(token)) {
System.out.println("請求頭沒有token");
return true;
}
System.out.println("拒接訪問");
JWTToken jwtToken = new JWTToken(token);
Subject subject = SecurityUtils.getSubject();
//進行認證
subject.login(jwtToken);
return true;
}
自定義實現shiro Realm
/**
* 繼承AuthorizingRealm類重寫doGetAuthorizationInfo(授權)
* doGetAuthenticationInfo(認證)
*/
public class CustomerRealm extends AuthorizingRealm {
/**
* 認證
* @param authenticationToken 認證token
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//獲取主體信息
JWTToken principal = (JWTToken) authenticationToken;
DecodedJWT verify;
//創建自定義principal並賦值
TokenPayload tokenPayload = new TokenPayload();
//解析token
try {
verify = JWTUtils.verify((String) principal.getPrincipal());
tokenPayload.setUserId(verify.getClaim("userId").asString());
tokenPayload.setRoles(verify.getClaim("roles").asString());
tokenPayload.setUserName(verify.getClaim("userName").asString());
tokenPayload.setPermissions(verify.getClaim("permissions").asString());
} catch (AlgorithmMismatchException exception) {
throw new AuthenticationException("算法不匹配異常" + exception.getMessage());
} catch (SignatureVerificationException exception) {
throw new AuthenticationException("簽名驗證異常" + exception.getMessage());
} catch (TokenExpiredException exception) {
throw new AuthenticationException("token過期異常" + exception.getMessage());
} catch (InvalidClaimException exception) {
throw new AuthenticationException("無效Claim異常" + exception.getMessage());
} catch (JWTDecodeException exception) {
throw new AuthenticationException("JWT解碼異常" + exception.getMessage());
} catch (JWTVerificationException exception) {
throw new AuthenticationException("JWT驗證異常" + exception.getMessage());
} catch (RuntimeException exception) {
throw new RuntimeException(exception.getMessage());
}
System.out.println("認證完成");
//將token解析過后的信息封裝成為主體傳入 授權時使用
return new SimpleAuthenticationInfo(tokenPayload, true, this.getName());
}
/**
* 授權
* @param principalCollection 授權主體
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("開始授權");
TokenPayload primaryPrincipal = (TokenPayload) principalCollection.getPrimaryPrincipal();
System.out.println(primaryPrincipal.getRoles());
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//添加角色信息
simpleAuthorizationInfo.addRole(primaryPrincipal.getRoles());
//添加權限信息
simpleAuthorizationInfo.addStringPermission(primaryPrincipal.getPermissions());
System.out.println("授權完成");
return simpleAuthorizationInfo;
}
@Override
public Class<?> getAuthenticationTokenClass() {
return JWTToken.class;
}
實現自定義的shiroToken
/**
* 為了便於使用由JWT生成的token 自定義實現自己的token
*/
public class JWTToken implements AuthenticationToken {
//存儲由請求頭中獲取的token
private final String jwtToken;
public JWTToken(String jwtToken) {
this.jwtToken = jwtToken;
}
@Override
public Object getPrincipal() {
return this.jwtToken;
}
@Override
public Object getCredentials() {
return true;
}
}
shiro配置
@Configuration
public class ShiroConfig {
/**
* 1.創建shiroFilterFactoryBean
* 負責攔截多有請求
*
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//給filter設置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwtfilter",new JWTFilter());
//將自定義過濾器加入到shiro 過濾其中
shiroFilterFactoryBean.setFilters(filters);
// 完全無狀態認證 noSessionCreation 不保留每次會話的session
//因此每次請求都會進行授權和認證
HashMap<String, String> map = new HashMap<>();
map.put("/shiro/login","anon");
map.put("/shiro/**","noSessionCreation,jwtfilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
shiroFilterFactoryBean.setLoginUrl("/shiro/unauthorized");
return shiroFilterFactoryBean;
}
/**
* 2.創建安全管理器
*
* @param realm
* @return
*/
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("myRealm") Realm realm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
/**
* 3.自定義Realm
*
* CredentialsMatcher 證書匹配器
* @return 自定義Realm
*/
@Bean("myRealm")
public Realm getRealm() {
CustomerRealm customerRealm = new CustomerRealm();
return customerRealm;
}
總結
使用JWTToken與shiro進無狀態授權認證時 實際上登錄時放棄的使用shiro的認證 登陸時使用自己實現的登錄方法並且生成Token,無狀態會話,用於shiro不存儲每次會話的session 因此每次請求都會進行一次完整的shiro授權認證流程 ,可以使用Redis 等其他緩存的方式實現shiro的緩存 減小系統壓力。