摘要:本文主要描述如何快速基於SpringBoot 2.5.X
版本整合Shiro+JWT
框架,讓大家快速實現無狀態登陸和介面許可權認證主體框架,具體業務細節未實現,大家按照實際專案補充。
背景
為什麼要使用
Shiro
?
隨大流吧,雖然自己也可以基於自定義註解+攔截器實現和
Shiro
一樣的功能,但是爲了適用於業界的規範,所以整合這個大家都能看得懂,而且Shiro
也相對簡單。
為什麼要用
Jwt
?
傳統的
session
模式越來越少,而且大多數系統都是微服務多客戶端的,所以無狀態的登陸更符合現階段的業務架構。
開始
本案例基於
SpringBoot 2.5.X + Shiro 1.8 + hutool的Jwt
。
pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.5.4</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.5.4</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.24</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.8.0</version> </dependency> </dependencies>
ResponseMessage
返回訊息體
@Data public class ResponseMessage<T> { private Boolean success = Boolean.TRUE; private String code; private String message; private T data; public static <T> ResponseMessage<T> success(T data){ ResponseMessage<T> responseMessage = new ResponseMessage<>(); responseMessage.setCode("200"); responseMessage.setMessage("操作成功"); responseMessage.setData(data); return responseMessage; } public static <T> ResponseMessage<T> fail(String message){ ResponseMessage<T> responseMessage = new ResponseMessage<>(); responseMessage.setSuccess(Boolean.FALSE); responseMessage.setCode("500"); responseMessage.setMessage(message); return responseMessage; } }
GlobalExceptionHandler
全域性異常處理
@Slf4j @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = Exception.class) @ResponseBody public ResponseMessage<String> handleAllExceptions(Exception ex, WebRequest request) { // 處理異常 log.error("業務異常",ex); return ResponseMessage.fail(ex.getMessage()); } }
JwtUtils
工具類
public class JwtUtils { private static final byte[] KEY = "ABADEXU".getBytes(); public static String createToken(Map<String, Object> payload){ return JWTUtil.createToken(payload, JwtUtils.KEY); } public static JWT parseToken(String token){ return JWTUtil.parseToken(token); } public static Boolean verify(String token){ return JWTUtil.verify(token, JwtUtils.KEY); } }
JwtToken
認證dto
類
@Data public class JwtToken implements AuthenticationToken { /** JWT 認證串 */ private String jwt; public JwtToken(String jwt) { this.jwt = jwt; } @Override public Object getPrincipal() { return jwt; } @Override public Object getCredentials() { return jwt; } }
JwtFilter
許可權認證過濾器
攔截請求介面的
@Slf4j public class JwtFilter extends AccessControlFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String jwt = httpServletRequest.getHeader("Authorization"); if(StrUtil.isNotBlank(jwt)){ getSubject(request, response).login(new JwtToken(jwt)); return true; } return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { throw new RuntimeException("身份驗證異常"); } }
JwtRealm
授權領域
授權流程
JwtFilter#isAccessAllowed -> JwtRealm#supports -> JwtRealm#doGetAuthenticationInfo -> JwtRealm#doGetAuthorizationInfo
@Slf4j public class JwtRealm extends AuthorizingRealm { @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { log.info("驗證jwt token 許可權"); String jwt = principalCollection.getPrimaryPrincipal().toString(); // 這裏一般就從redis中拿使用者的許可權資訊,案例就直接寫死了 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 設定角色 simpleAuthorizationInfo.addRole("admin"); simpleAuthorizationInfo.addRole("user"); // 設定許可權 simpleAuthorizationInfo.addStringPermission("user:add"); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.info("驗證jwt token 有效性"); String jwt = authenticationToken.getPrincipal().toString(); // 從redis查詢jwt token是否還存在,是否有效 if(!Boolean.TRUE.equals(JwtUtils.verify(jwt))){ throw new RuntimeException("jwt token 失效"); } JWT parseToken = JwtUtils.parseToken(jwt); Object expiryTime = parseToken.getPayload("expiryTime"); // 驗證token是否過期 return new SimpleAuthenticationInfo(jwt, jwt, this.getClass().getName()); } }
ShiroConfig
配置
@Configuration public class ShiroConfig { @Bean public JwtRealm jwtRealm(){ return new JwtRealm(); } @Bean public DefaultWebSecurityManager defaultWebSecurityManager() { DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); defaultWebSecurityManager.setRealm(jwtRealm()); /* * 關閉shiro自帶的session,詳情見文件 * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29 */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); defaultWebSecurityManager.setSubjectDAO(subjectDAO); return defaultWebSecurityManager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(defaultWebSecurityManager()); // 未授權跳轉 shiroFilter.setUnauthorizedUrl("/unauthorized"); Map<String, Filter> filterMap = new HashMap<>(); // filterMap.put("jwt", new JwtFilter()); // shiroFilter.setFilters(filterMap); Map<String, String> filterRuleMap = new LinkedHashMap<>(); // 匿名訪問 filterRuleMap.put("/error", "anon"); filterRuleMap.put("/login", "anon"); filterRuleMap.put("/logout", "anon"); filterRuleMap.put("/unauthorized", "anon"); // 登入並具有 admin 角色 // filterRuleMap.put("/index/admin", "authc,roles[admin]"); // filterRuleMap.put("/index/admin", "jwt,roles[admin]"); // 透過jwt校驗,需登入才能訪問(自行實現邏輯) filterRuleMap.put("/**", "jwt"); // shiroFilter.setFilterChainDefinitionMap(filterRuleMap); // return shiroFilter; } /** * 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions) * @return */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setUsePrefix(true); return advisorAutoProxyCreator; } }
測試相關
LoginController
登陸介面
@RestController public class LoginController { @GetMapping("login") public Object login(String username){ Map<String, Object> payload = new HashMap<>(); payload.put("username", username); // 設定30分鐘後過期 payload.put("expiryTime", DateUtil.date().offset(DateField.MINUTE, 30)); return JwtUtils.createToken(payload); } }
UserController
許可權驗證介面
@Slf4j @RestController @RequestMapping(value = "user") public class UserController { @RequiresPermissions(value = {"user:view"}) @GetMapping(value = "page") public Object page(){ log.info("page"); return "SUCCESS"; } @RequiresPermissions(value = {"user:add"}) @GetMapping(value = "add") public Object add(){ log.info("add"); return "SUCCESS"; } }