Shiro-JWT SpringBoot前後端分離許可權認證的一種思路
阿新 • • 發佈:2020-09-12
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的快取 減小系統壓力。