swagger◆jwt◆shiro◆redis
最近在项目中想整合swagger◆jwt◆shiro◆redis过程中遇到诸多问题和困难,现重新写一个demo并记录解决步骤.存在的问题:
shiro默认的拦截跳转都是跳转url页面,而前后端分离后,后端并无权干涉页面跳转.
shiro默认的登录拦截校验机制是使用的session.
参考资料:SpringBoot结合JWT◆Shiro◆Redis实现token无状态登录授权
需求首次通过post请求进行登入;
登录成功后返回token;
服务端负责token生命周期的刷新
用户权限的校验;
Shiro ◆ JWT实现无状态鉴权机制
首先post用户名与密码到login进行登入,如果成功在请求头Header返回一个加密的Authorization,失败的话直接返回未登录,以后访问都带上这个Authorization即可.
鉴权流程主要是要重写shiro的入口过滤器BasicHttpAuthenticationFilter,在此基础上进行拦截、token验证授权等操作
关于AccessToken及RefreshToken概念说明
关于Redis中保存RefreshToken信息(做到JWT的可控性)
登录认证通过后返回AccessToken信息(在AccessToken中保存当前的时间戳和帐号),同时在Redis中设置一条以帐号为Key,Value为当前时间戳(登录时间)的RefreshToken,现在认证时必须AccessToken没失效以及Redis存在所对应的RefreshToken,且RefreshToken时间戳和AccessToken信息中时间戳一致才算认证通过,这样可以做到JWT的可控性,如果重新登录获取了新的AccessToken,旧的AccessToken就认证不了,因为Redis中所存放的的RefreshToken时间戳信息只会和最新的AccessToken信息中携带的时间戳一致,这样每个用户就只能使用最新的AccessToken认证.
Redis的RefreshToken也可以用来判断用户是否在线,如果删除Redis的某个RefreshToken,那这个RefreshToken所对应的AccessToken之后也无法通过认证了,就相当于控制了用户的登录,可以剔除用户
关于根据RefreshToken自动刷新AccessToken
同时前端进行获取替换,下次用新的AccessToken进行访问即可.
1.8
③③2
1.⑥0
2.9.2
③9.0
org.springframework.boot
spring-boot-starter-data-jdbc
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
org.springframework.boot
spring-boot-starter-logging
com.auth0
java-jwt
${jwt.version}
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.version}
org.springframework.boot
spring-boot-starter-data-redis
org.projectlombok
lombok
org.apache.shiro
shiro-spring
${shiro.version}
io.springfox
springfox-swagger2
${swagger.version}
io.springfox
springfox-swagger-ui
${swagger.version}
mysql
mysql-connector-java
runtime
配置文件
server:
port: 8081
jwt:
# AccessToken 过期时间单位分钟
EXPIRE_TIME: 5
# RefreshToken 过期时间单位分钟
REFRESH_EXPIRE_TIME: 30
# 密钥盐
TOKEN_SECRET: 50eFa4d9W8ba4d*bb276f^11
swagger:
enabled: true
spring:
datasource:
username: root
password: maple1234
url: jdbc:mysql://12⑦0.0.1:3306/db2020?useSSL=falseuseUnicode=truecharacterEncoding=utf-8serverTimezone=GMT+8
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 12⑦0.0.1
port: 6379
mybatis-plus:
mapper-locations: classpath:com/maplexl/shiroJwtRedis/mapper/xml/*.xml
logging:
level:
com.maplexl: debug
@Configuration
@EnableSwagger2
public class SwaggerConfig {
//是否开启swagger,根据环境来选择
@Value(value = "${swagger.enabled}")
Boolean swaggerEnabled;
@Bean
public Docket api() {
//全局参数
Parameter token = new ParameterBuilder().name("token")
.description("用户登陆令牌")
.parameterType("header")
.modelRef(new ModelRef("String"))
//是否必须
.required(false)
.build();
ArrayList parameters = new ArrayList<>();
parameters.add(token);
return new Docket(DocumentationType.SWAGGER_2)
.globalOperationParameters(parameters)
.apiInfo(apiInfo())
.enable(swaggerEnabled)
.groupName("api")
.select()
.apis(RequestHandlerSelectors.basePackage("com.maplexl.shiroJwtRedis.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("jwt-shiro-redis")
.description("api")
// 作者信息
.contact(new Contact("枫叶", "https://www.cnblogs.com/junlinsky/", "203051919@qq.com"))
.version("1.0.0")
.build();
}
}
主要实现token的签发、验证和数据解析.
@Slf4j
@SuppressWarnings("unused")
public class JwtUtil {
/**
* token到期时间,毫秒为单位
*/
public static long EXPIRE_TIME;
/**
* RefreshToken到期时间为,秒为单位
*/
public static long REFRESH_EXPIRE_TIME;
/**
* 密钥盐
*/
private static String TOKEN_SECRET;
/**
* 设置token过期时间及密钥盐
*
* @param expireTime 客户端token过期时间
* @param refreshExpireTime 服务器token过期时间
* @param tokenSecret token加密使用的盐值
*/
public static void setProperties(long expireTime, long refreshExpireTime, String tokenSecret) {
JwtUtil.EXPIRE_TIME = expireTime;
JwtUtil.REFRESH_EXPIRE_TIME = refreshExpireTime;
JwtUtil.TOKEN_SECRET = tokenSecret;
}
/**
* 校验token是否正确
*
* @param token 密钥
* @return 是否正确
*/
public static boolean verify(String token) {
//创建token验证器
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).build();
DecodedJWT decodedjwt = jwtVerifier.verify(token);
log.info("认证通过");
log.info("userId: [{}]", decodedjwt.getClaim("userId").asLong());
log.info("过期时间: [{}]", decodedjwt.getExpiresAt());
return true;
}
/**
* 获得token中的信息
*
* @return token中包含的用户id
*/
public static long getUserId(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("userId").asLong();
} catch (JWTDecodeException e) {
return -1L;
}
}
/**
* 获取currentTime
* @param token 密钥
* @return currentTime
*/
public static Long getCurrentTime(String token){
try{
DecodedJWT decodedjwt =JWT.decode(token);
return decodedjwt .getClaim("currentTime").asLong();
}catch (JWTCreationException e){
return null;
}
}
/**
* 根据request中的token获取currentTime
*
* @param request request
* @return CurrentTime
*/
public static Long getCurrentTime(HttpServletRequest request) {
String accessToken = request.getHeader("token");
Long currentTime = getCurrentTime(accessToken);
if (currentTime == null) {
throw new RuntimeException("未获取到currentTime");
}
return currentTime;
}
/**
* 生成签名,5min后过期
*
* @param userId 用户名
* @return 加密的token
*/
public static String sign(Long userId, Long currentTime) {
String token = null;
try {
Date expireAt = new Date(currentTime ◆ EXPIRE_TIME);
token = JWT.create()
//存放数据
.withClaim("userId", userId)
.withClaim("currentTime", currentTime)
//过期时间
.withExpiresAt(expireAt)
.sign(Algorithm.HMAC256(TOKEN_SECRET));
} catch (RuntimeException e) {
e.printStackTrace();
}
return token;
}
/**
* 根据request中的token获取账号
*
* @param request request
* @return 用户的账号
*/
public static Long getUserId(HttpServletRequest request) {
String accessToken = request.getHeader("token");
return getUserId(accessToken);
}
}
上面的工具类在使用前需需要设置EXPIRE_TIME、TOKEN_SECRET和REFRESH_EXPIRE_TIME
配置类
@Configuration
public class JwtUtilConfig {
@Value("${jwt.EXPIRE_TIME}")
private Long expireTime;
@Value("${jwt.REFRESH_EXPIRE_TIME}")
private Long refreshExpireTime;
@Value("${jwt.TOKEN_SECRET}")
private String tokenSecret;
@PostConstruct
public void init() {
JwtUtil.setProperties(expireTime * 1000 * 60, refreshExpireTime * 60, tokenSecret);
}
}
RedisConfig
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer
RedisUtil
@SuppressWarnings("unused")
public final class RedisUtil {
private static RedisTemplate redisTemplate;
public static void setRedisTemplate(RedisTemplate redisTemplate) {
RedisUtil.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public static void expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public static Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public static Boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static boolean hasKey(long key) {
return hasKey(Long.toString(key));
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("all")
public static void del(String... key) {
if (key != null key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public static Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
public static Object get(long key) {
return get(Long.toString(key));
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
*/
public static void set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
*/
public static void set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
*/
public static void set(long key, Object value, long time) {
set(Long.toString(key),value,time);
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public static Long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public static Long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public static Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public static Map
判断请求接口是否需要进行登录认证授权,如果需要则该请求就必须在Header中添加token字段存AccessToken,无需授权则直接访问.
需要授权的接口就调用getSubject(request, response).login(token),将AccessToken提交给shiro中的CustomRealm进行认证.
AccessToken刷新:判断RefreshToken是否过期,未过期就返回新的AccessToken及RefreshToken并让请求继续正常访问.
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 判断是否允许通过
*
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
log.info("isAccessAllowed方法");
try{
return executeLogin(request,response);
}catch (Exception e){
log.info("错误"◆e);
responseError(response,"shiro fail");
return false;
}
}
/**
* 是否进行登录请求
*
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
log.info("isLoginAttempt方法");
String token=((HttpServletRequest)request).getHeader("token");
return token != null;
}
/**
* 创建shiro token
*
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
log.info("createToken方法");
String jwtToken = ((HttpServletRequest)request).getHeader("token");
if(jwtToken!=null) {
return new JwtToken(jwtToken);
}
return null;
}
/**
* isAccessAllowed为false时调用,验证失败
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
log.info("onAccessDenied");
this.sendChallenge(request,response);
responseError(response,"token verify fail");
return false;
}
/**
* shiro验证成功调用
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) {
log.info("onLoginSuccess:");
String jwttoken= (String) token.getPrincipal();
if (jwttoken!=null){
try{
if(JwtUtil.verify(jwttoken)){
//判断Redis是否存在所对应的RefreshToken
long userId = JwtUtil.getUserId(jwttoken);
Long currentTime=JwtUtil.getCurrentTime(jwttoken);
if (RedisUtil.hasKey(userId)) {
Long currentTimeMillisRedis = (Long) RedisUtil.get(userId);
return currentTimeMillisRedis.equals(currentTime);
}
}
return false;
}catch (Exception e){
log.info("token验证:"◆e.getClass());
if (e instanceof TokenExpiredException){
log.info("TokenExpiredException");
return refreshToken(request, response);
}
}
}
return true;
}
/**
* 拦截器的前置方法,此处进行跨域处理
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
HttpServletResponse httpServletResponse= (HttpServletResponse) response;
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-Resquest-Headers"));
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
httpServletResponse.setStatus(HttpStatus.OK.value());
}
//如果不带token,不去验证shiro
if (!isLoginAttempt(request,response)){
responseError(httpServletResponse,"no token");
return false;
}
return super.preHandle(request,response);
}
/**
* 刷新AccessToken,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
*/
private boolean refreshToken(ServletRequest request, ServletResponse response) {
String token = ((HttpServletRequest)request).getHeader("token");
long userId = JwtUtil.getUserId(token);
Long currentTime=JwtUtil.getCurrentTime(token);
// 判断Redis中RefreshToken是否存在
if (RedisUtil.hasKey(userId)) {
// Redis中RefreshToken还存在,获取RefreshToken的时间戳
Long currentTimeMillisRedis = (Long) RedisUtil.get(userId);
// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
if (currentTimeMillisRedis.equals(currentTime)) {
// 获取当前最新时间戳
Long currentTimeMillis =System.currentTimeMillis();
RedisUtil.set(userId, currentTimeMillis,JwtUtil.REFRESH_EXPIRE_TIME);
// 刷新AccessToken,设置时间戳为当前最新时间戳
token = JwtUtil.sign(userId, currentTimeMillis);
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Authorization", token);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
return true;
}
}
return false;
}
private void responseError(ServletResponse response, String msg){
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(401);
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json;charset=UTF-8");
try {
String rj = new ObjectMapper().writeValueAsString(new Result<>(401,msg));
httpResponse.getWriter().append(rj);
} catch (IOException e) {
e.printStackTrace();
}
}
}
JwtFilter需要用到的几个类说明
Result
JwtToken:shiro中Subje.login接收参数为AuthenticationToken,所以我们需要对我们的token进行封装.
这里解析一下该类的执行流程:首先需要授权的请求经过preHandle进行跨域处理后进入isAccessAllowed方法,isAccessAllowed方法直接调用BasicHttpAuthenticationFilter类的父类AuthenticatingFilter中executeLogin方法,executeLogin方法源码如下:
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = this.createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
} else {
try {
Subject subject = this.getSubject(request, response);
subject.login(token);
return this.onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException var5) {
return this.onLoginFailure(token, var5, request, response);
}
}
}
该方法会先调用createToken方法创建token,然后调用this.getSubject(request, response)进行shiro授权但subject.login参数类型为AuthenticationToken和我们的token不一致,所以呢我们需要重写createToken方法,并创建一个实现了AuthenticationToken接口的JwtToken包装类
JwtToken
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
executeLogin方法后,授权成功会进入onLoginSuccess方法,该方法进行token的检验,token的检验失败进入onAccessDenied.
@Component
@Slf4j
public class MyReailm extends AuthorizingRealm {
@Resource
UserMapper userMapper;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 用户授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("用户授权");
long userId = JwtUtil.getUserId(principalCollection.toString());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
User user = userMapper.selectById(userId);
Integer authority = user.getAuthority();
Set role = new HashSet<>();
/*
* 权限说明:2查看权限,3查看和编辑权限,
* 6查看和授权权限,7查看、编辑、授权权限
*/
switch (authority) {
case 2:
role.add("view");
break;
case 3:
role.add("view");
role.add("edit");
break;
case 6:
role.add("view");
role.add("authorization");
break;
case 7:
role.add("view");
role.add("edit");
role.add("authorization");
break;
default:
role.add("tourist");
}
//设置角色集合
//设置权限方式与设置角色类似使用info.setStringPermissions();
info.setRoles(role);
return info;
}
/**
* 用户身份认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("身份认证");
String token = (String) authenticationToken.getCredentials();
long userId = JwtUtil.getUserId(token);
log.info(userId◆"");
User user = userMapper.selectById(userId);
if (userId <= 0 || user == null) {
throw new AuthenticationException("认证失败!");
}
return new SimpleAuthenticationInfo(token, token, "MyRealm");
}
}
Shiro配置ShiroConfig
import com.maplexl.shiroJwtRedis.bean.MyReailm;
import com.maplexl.shiroJwtRedis.filter.JwtFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* shiro 配置类
*/
@Configuration
public class ShiroConfig {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 过滤器
Map filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不会被拦截的链接 顺序判断
// 登录接口排除
filterChainDefinitionMap.put("/login", "anon");
// 注册接口排除
filterChainDefinitionMap.put("/register", "anon");
//登出接口排除
filterChainDefinitionMap.put("/logout", "anon");
//欢迎页排除
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
filterChainDefinitionMap.put("/**/*.css", "anon");
filterChainDefinitionMap.put("/**/*.html", "anon");
filterChainDefinitionMap.put("/**/*.jpg", "anon");
filterChainDefinitionMap.put("/**/*.png", "anon");
filterChainDefinitionMap.put("/**/*.ico", "anon");
//swagger资源排除
filterChainDefinitionMap.put("/swagger-ui.html/**","anon");
filterChainDefinitionMap.put("/swagger-resources/**","anon");
filterChainDefinitionMap.put("/v2/**","anon");
// 添加自己的过滤器并且取名为jwt
Map filterMap = new HashMap<>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 过滤链定义,从上向下顺序执行,一般将放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
//未授权界面返回JSON
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(MyReailm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications(Sessionless)
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 下面的代码是添加注解支持
*/
@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;
}
}
@ControllerAdvice
@ResponseBody
@SuppressWarnings("unused")
public class GlobalExceptionHandler {
/**
* 捕捉所有Shiro异常
*/
@ExceptionHandler(ShiroException.class)
public Result handle401(ShiroException e) {
return new Result<>(401, "无权访问(Unauthorized):" ◆ e.getMessage());
}
/**
* 单独捕捉Shiro(UnauthorizedException)异常 该异常为访问有权限管控的请求而该用户没有所需权限所抛出的异常
*/
@ExceptionHandler(UnauthorizedException.class)
public Result handle401(UnauthorizedException e) {
return new Result<>(401, "无权访问(Unauthorized):当前Subject没有此请求所需权限(" ◆ e.getMessage() ◆ ")");
}
/**
* 单独捕捉Shiro(UnauthenticatedException)异常
* 该异常为以游客身份访问有权限管控的请求无法对匿名主体进行授权,而授权失败所抛出的异常
*/
@ExceptionHandler(UnauthenticatedException.class)
public Result handle401(UnauthenticatedException e) {
e.printStackTrace();
return new Result<>(401, "无权访问(Unauthorized):当前Subject是匿名Subject,请先登录(This subject is anonymous.)");
}
/**
* 捕捉校验异常(BindException)
*/
@ExceptionHandler(BindException.class)
public Result validException(BindException e) {
List fieldErrors = e.getBindingResult().getFieldErrors();
Map error = this.getValidError(fieldErrors);
return new Result<>(400, error.get("errorMsg").toString(), error.get("errorList"));
}
/**
* 捕捉404异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
public Result handle(NoHandlerFoundException e) {
return new Result<>(404, e.getMessage());
}
/**
* 捕捉其他所有异常
*/
@ExceptionHandler(Exception.class)
public Result globalException(HttpServletRequest request, Throwable ex) {
return new Result<>(500, ex.toString() ◆ ": " ◆ ex.getMessage());
}
/**
* 获取状态码
*/
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
return HttpStatus.valueOf(statusCode);
}
/**
* 获取校验错误信息
*/
private Map getValidError(List fieldErrors) {
Map map = new HashMap<>(16);
List errorList = new ArrayList<>();
StringBuffer errorMsg = new StringBuffer("校验异常(ValidException):");
for (FieldError error : fieldErrors) {
errorList.add(error.getField() ◆ "-" ◆ error.getDefaultMessage());
errorMsg.append(error.getField()).append("-").append(error.getDefaultMessage()).append(".");
}
map.put("errorList", errorList);
map.put("errorMsg", errorMsg);
return map;
}
}
@RestController
@Slf4j
public class LoginController {
@Resource
UserService userService;
/**
* 登录接口
* @param user 用户
* @param response 响应
* @return 登录成功则返回token
*/
@ApiOperation("登录接口")
@PostMapping("/login")
public Result login(@RequestBody User user, HttpServletResponse response) {
String userName = user.getUserName();
String password = user.getPassword();
if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
return new Result<>(CommonEnum.INVALID_CHARACTER);
}
//查询是否有此用户
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("user_name", userName);
User dbUser = userService.getOne(wrapper);
if (dbUser == null) {
return new Result<>(CommonEnum.NO_SUCH_USER);
}
//校验密码
password = new SimpleHash("MD5", password, dbUser.getSalt(), 32).toString();
if (password.equals(dbUser.getPassword())) {
//密码正确
long timeMillis = System.currentTimeMillis();
String token = JwtUtil.sign(dbUser.getUserId(), timeMillis);
dbUser.setPassword(null);
//token放入redis
RedisUtil.set(dbUser.getUserId(), timeMillis, JwtUtil.REFRESH_EXPIRE_TIME);
response.setHeader("Authorization", token);
response.setHeader("Access-Control-Expose-Headers", "Authorization");
return new Result<>(CommonEnum.SUCCESS, dbUser);
}
return new Result<>(CommonEnum.PASSWORD_ERROR);
}
/**
* 添加用户,即注册
*
* @param user 用户信息
* @return 是否保存成功
*/
@ApiOperation("注册接口")
@PostMapping("/register")
public Result add(@RequestBody User user) {
//验证用户名是否重复
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("user_name",user.getUserName());
User dbUser = userService.getOne(wrapper);
if (dbUser!=null){
return new Result<>(CommonEnum.USER_EXIST);
}
//加密用户的密码
String salt = UUID.randomUUID().toString().substring(0, 24);
String password = new SimpleHash("MD5", user.getPassword(), salt, 32).toString();
user.setSalt(salt);
user.setPassword(password);
boolean save = userService.save(user);
if (save) {
return new Result<>(CommonEnum.SUCCESS, true);
}
return new Result<>(CommonEnum.INVALID_INSERT, false);
}
/**
* 登出
* @return 是否登出
*/
@GetMapping("/logout")
public Result logout(HttpServletRequest request){
/*
* 清除redis中的RefreshToken即可
*/
Long userId = JwtUtil.getUserId(request);
RedisUtil.del(Long.toString(userId));
return new Result<>(CommonEnum.SUCCESS);
}
}
UserController用于对用户CRUD
@RestController
public class UserController {
@Resource
UserService userService;
/**
* 根据id删除用户
*
* @param userId 用户id
* @return 是否删除成功
*/
@RequiresRoles("edit")
@DeleteMapping("/user/{id}")
public Result delete(@PathVariable("id") Long userId) {
boolean del = userService.removeById(userId);
if (del) {
return new Result<>(CommonEnum.SUCCESS, true);
}
return new Result<>(CommonEnum.INVALID_DELETE, false);
}
/**
* 修改用户信息
*
* @param user 需修改的信息
* @return 是否成功
*/
@RequiresRoles("edit")
@PutMapping("/user")
public Result update(@RequestBody User user) {
boolean up = userService.updateById(user);
if (up) {
return new Result<>(CommonEnum.SUCCESS, true);
}
return new Result<>(CommonEnum.INVALID_UPDATE, false);
}
/**
* 根据用户id查询用户
*
* @param userId 用户id
* @return 用户信息
*/
@RequiresRoles("view")
@GetMapping("/user/{id}")
public Result getById(@PathVariable("id") Long userId) {
User user = userService.getById(userId);
if (user != null) {
return new Result<>(CommonEnum.SUCCESS, user);
}
return new Result<>(CommonEnum.NO_SUCH_USER);
}
/**
* 获取用户列表
*
* @return 用户列表
*/
@RequiresRoles("view")
@GetMapping("/user/list")
public Result> getList() {
List list = userService.list();
return new Result<>(CommonEnum.SUCCESS, list);
}
}
有token(view身份)
删除操作
可以看到,请求是成功的,并且在响应头里返回了新的token
解决方案如下
配置jackson将Long序列化成字符串
@JsonComponent
public class JsonSerializerManage {
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(module);
return objectMapper;
}
}
最后贴上项目目录结构和git地址
以上就是土嘎嘎小编为大家整理的swagger◆jwt◆shiro◆redis相关主题介绍,如果您觉得小编更新的文章只要能对粉丝们有用,就是我们最大的鼓励和动力,不要忘记讲本站分享给您身边的朋友哦!!