摘要:本文主要描述如何快速基于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"; } }