摘要:本文主要描述如何快速基于SpringBoot 2.5.X
版本集成Shiro+JWT
框架,让大家快速实现无状态登陆和接口权限认证主体框架,具体业务细节未实现,大家按照实际项目补充。
背景
- 为什么要使用
Shiro
?
随大流吧,虽然自己也可以基于自定义注解+拦截器实现和
Shiro
一样的功能,但是为了适用于业界的规范,所以集成这个大家都能看得懂,而且Shiro
也相对简单。
- 为什么要用
Jwt
?
传统的
session
模式越来越少,而且大多数系统都是微服务多客户端的,所以无状态的登陆更符合现阶段的业务架构。
开始
本案例基于
SpringBoot 2.5.X + Shiro 1.8 + hutool的Jwt
。
pom.xml
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
返回消息体
typescript
@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
全局异常处理
less
@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
工具类
typescript
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
类
typescript
@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
权限认证过滤器
拦截请求接口的
scala
@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
java
@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
配置
typescript
@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
登陆接口
typescript
@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
权限验证接口
less
@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";
}
}