Springboot+JWT+Shiro集成完全版(带测试示例)
Posted 东北_老乡
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Springboot+JWT+Shiro集成完全版(带测试示例)相关的知识,希望对你有一定的参考价值。
相信大家已经对shiro,jwt有基本的概念了,不熟悉的可以看下
jwt:https://blog.csdn.net/Goligory/article/details/104400381
对于shiro等会我贴上代码然后简单分析下
maven引入
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
</dependency>
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
</dependency>
import com.mtgg.laoxiang.common.constant.CommonConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Author: lǎo xiāng
* @Date: 2021/2/4 17:56
* @Describe: 鉴权登录拦截器
*/
@Slf4j
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter {
private boolean allowOrigin = true;
public JwtFilter(){}
public JwtFilter(boolean allowOrigin){
this.allowOrigin = allowOrigin;
}
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
throw new AuthenticationException("Token失效,请重新登录", e);
}
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if(allowOrigin){
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 是否允许发送Cookie,默认Cookie不包括在CORS请求之中。设为true时,表示服务器允许Cookie包含在请求中。
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
}
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
//多租户用到
// String tenant_id = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
// TenantContext.setTenant(tenant_id);
return super.preHandle(request, response);
}
}
这个类主要做的事:当有访问携带token过来的时候会走isAccessAllowed认证,由executeLogin交给shiro进行验证
那么验证过程中shiro和jwt token是如何进行关联的呢?看下面
import org.apache.shiro.authc.AuthenticationToken;
/**
* @Author: lǎo xiāng
* @Date: 2021/2/4 17:56
* @Describe: 实现AuthenticationToken 使Realm的doGetAuthenticationInfo能够获取到token进行验证
*/
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
AuthenticationToken是shiro-core包下的接口,实现后可以用getPrincipal获取到我们的token,这样shiro就拿到了
下面再加入配置对接口的过滤等配置,其中可以设置登录,过滤路径,注意看要添加自定义Filter,JwtFilter就是在此时被加载生效
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.*;
/**
* @Author: lǎo xiāng
* @Date: 2021/2/4 17:57
* @Describe: shiro 配置类
*/
@Slf4j
@Configuration
public class ShiroConfig {
/**
* Filter Chain定义说明
*
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/login/notLogin");
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/login/notRole");
// 设置拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//游客,开发权限 TODO 暂时设置所有都不拦截
filterChainDefinitionMap.put("/bg/**", "anon");
filterChainDefinitionMap.put("/start/**", "anon");
filterChainDefinitionMap.put("/test/**", "anon");
//swagger接口权限 开放
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
// filterChainDefinitionMap.put("/elastic/**", "anon");
//用户,需要角色权限 “user”
filterChainDefinitionMap.put("/user/**", "roles[user]");
//管理员,需要角色权限 “admin”
filterChainDefinitionMap.put("/admin/**", "roles[admin]");
//其余接口一律拦截
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "authc");
//添加自定义过滤器并取名jwt
Map<String, Filter> map = new HashMap<>(1);
map.put("jwt",new JwtFilter());
shiroFilterFactoryBean.setFilters(map);
// //所有请求通过我们自己的JWT Filter
filterChainDefinitionMap.put("/**", "jwt");
// 访问 /unauthorized/** 不通过JWTFilter
filterChainDefinitionMap.put("/unauthorized/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(CustomRealm customRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm);
return securityManager;
}
//将自己的验证方式加入容器
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
return customRealm;
}
/**
* 下面的代码是添加注解支持
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
好了,shiro+jwt+springboot已经配置好了,那么如何使用呢?先看登录
@ApiOperation(value = "登录注册", notes = "校验,注册,生成token")
@PostMapping("/login")
public Result<LoginDTO> login(@RequestBody LoginReq loginReq) {
log.info("登录注册:{}", JSONObject.toJSONString(loginReq));
String phone = loginReq.getPhone();
Result check = check(loginReq);
if (!check.isSuccess()) {
return check;
}
Users user = usersService.login(loginReq);
AuthInfo authInfo = new AuthInfo();
authInfo.setPhone(phone);
authInfo.setUserId(user.getId());
authInfo.setUsername(user.getUsername());
//TODO 暂时不用shiro验证,后续加入
String token = JwtUtil.signInfo(authInfo, phone);
boolean set = redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, String.valueOf(user.getId()), JwtUtil.EXPIRE_SECOND - 1);
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername(user.getUsername());
loginDTO.setHeadImg(user.getHeadImg());
loginDTO.setToken(token);
log.info("登录注册返回loginDTO:{}", loginDTO);
return Result.success(loginDTO);
}
这里就联系上了,check是进行一些检验,通过后可以用userService.login来检查登录注册,没有可以创建账号
账号有了,也通过了,最后生成token,可以封装一个对象放到value中,这样扩展性比较好,注意redis的时间要比token的时间短一点
登出
@ApiOperation(value = "登出")
@GetMapping("/logout")
public Result logout(HttpServletRequest request){
String token = request.getHeader(KeyConstant.TOKEN);
Subject lvSubject= SecurityUtils.getSubject();
lvSubject.logout();
redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token);
return Result.success();
}
权限验证如下,其中什么角色还是权限在 CustomRealm的 doGetAuthorizationInfo 中加进去
@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
有user或admin角色
@RequiresPermissions("vip")
有vip权限
坑
不知道看没看到我的注释,CustomRealm这个类有一个注入注释,如果使用UserService会导致UserService的事务失效,可以新建一个类来查询处理:也就是说如果注入了会导致UserServiceImpl中@Transationl失效,具体为什么可以查一下,本人没细看
解决方案有二
1.加上@Lazy注解,延迟注入
2.用另一个Service专门来做注入处理
好,那么具体是什么执行流程呢?我给大家演示一下
首先,当然是登录了,登录后会返回一个token,后续请求中按约定携带token
可以看到先走的是自定义JwtFilter的验证方法
接下来走的是JwtToken的构造方法赋值token,这样就给了shiro;
接下来执行
getSubject(request, response).login(jwtToken);
可以看到开始通过CustomRealm验证了,首先拿到innfo,验证token有效性,中间其实可以加入其它验证
这里的设计是为了解决用户操作中失效的问题,具体解释看代码更详细,如果大于3小时可以不做延长时间处理
校验token有效性表示:如果redis中有token,jwt中token失效了,那么重新生成并设置时间(当然正常来说不会,因为redis时间可以设置比token少一点使jwt中的token一定晚于redistoken失效)
权限的我就不执行了
祝我们不忘初心,方得始终
以上是关于Springboot+JWT+Shiro集成完全版(带测试示例)的主要内容,如果未能解决你的问题,请参考以下文章
带有 JWT 的 Spring Boot 和 Apache Shiro - 我使用正确吗?
springboot shiro和freemarkervuejs/element-ui集成之控制按钮权限完全参考手册