版权声明:LT https://blog.csdn.net/LitongZero/article/details/88177716
SpringBoot2.0 添加Token、Redis
背景
1.由于前后端分离式开发,以及安全性等等问题,使用Token来进行回话的认证,已经成为了一个非常常用的操作
其他的优点啥的,我就不再赘述,本文主要考虑实现。
文章目录
1.开发环境
①. SpringBoot
2.1.0
②. jdk
1.8
③. IDEA
④. …
2.引入jjwt
依赖
pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
3.写一个生成,解析Token的工具类
TokenUtil.java
工具类附带一个测试方法
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
/**
* @Author: litong
* @Date: 2019/2/28 11:24
* @Description:
*/
public class TokenUtil {
/**
* 签名秘钥,可以换成 秘钥 注入
*/
public static final String SECRET = "LiTongZero";
/**
* 签发地
*/
public static final String issuer = "litongzero.com";
/**
* 过期时间
*/
public static final long ttlMillis = 3600*1000*60;
/**
* 生成token
*
* @param id 一般传入userName
* @return
*/
public static String createJwtToken(String id,String subject) {
return createJwtToken(id, issuer, subject, ttlMillis);
}
public static String createJwtToken(String id) {
return createJwtToken(id, issuer, "", ttlMillis);
}
/**
* 生成Token
*
* @param id 编号
* @param issuer 该JWT的签发者,是否使用是可选的
* @param subject 该JWT所面向的用户,是否使用是可选的;
* @param ttlMillis 签发时间 (有效时间,过期会报错)
* @return token String
*/
public static String createJwtToken(String id, String issuer, String subject, long ttlMillis) {
// 签名算法 ,将对token进行签名
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成签发时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 通过秘钥签名JWT
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// 让我们设置JWT声明
JwtBuilder builder = Jwts.builder().setId(id)
.setIssuedAt(now)
.setSubject(subject)
.setIssuer(issuer)
.signWith(signatureAlgorithm, signingKey);
// if it has been specified, let's add the expiration
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
// 构建JWT并将其序列化为一个紧凑的url安全字符串
return builder.compact();
}
/**
* Token解析方法
* @param jwt Token
* @return
*/
public static Claims parseJWT(String jwt) {
// 如果这行代码不是签名的JWS(如预期),那么它将抛出异常
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
.parseClaimsJws(jwt).getBody();
return claims;
}
public static void main(String[] args) {
String token = TokenUtil.createJwtToken("1","ltz");
System.out.println(TokenUtil.createJwtToken("1","ltz"));
Claims claims = TokenUtil.parseJWT(token);
System.out.println(claims);
}
}
4.自定义两个注解
①.是否需要验证Token
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @BelongsProject: JDTaste
* @BelongsPackage: com.jdtaste.common.util
* @Author:
* @CreateTime: 2019-03-04 15:38
* @Description: 在需要登录验证的Controller的方法上使用此注解
*/
@Target({ElementType.METHOD})// 可用在方法名上
@Retention(RetentionPolicy.RUNTIME)// 运行时有效
public @interface LoginRequired {
}
②.注入用户信息
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @BelongsProject: JDTaste
* @BelongsPackage: com.jdtaste.common.util
* @Author:
* @CreateTime: 2018-07-04 15:39
* @Description: 在Controller的方法参数中使用此注解,该方法在映射时会注入当前登录的User对象
*/
@Target(ElementType.PARAMETER) // 可用在方法的参数上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
public @interface CurrentUser {
}
5.Login方法
LoginController.java
/**
* 用户登录
* @param user
* @return
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Map<String,Object> userLogin(@RequestBody User user) {
Map<String,Object> map = null;
// 根据用户名查找用户方法
User userByUname = loginService.findUserByUname(user);
if (userByUname == null) {
map.put("code","1001");
map.put("msg","用户不存在");
return map;
}else {
User result ;
// 根据用户名和密码查找用户方法
result = loginService.checkPassword(user);
if (result == null) {
map.put("code","1002");
map.put("msg","密码错误");
return map;
} else {
// 登录方法
User login = loginService.login(result);
map.put("code","1000");
map.put("msg","success");
map.put("data",login);
return map;
}
}
}
User.java
private Long id;
private String token;
private String uname;
private String password;
LoginServiceImpl.java
public User login(User user) {
//生成Token
String accessToken= TokenUtil.createJwtToken(user.getUname());
user.setToken(accessToken);
//登录成功,将Token最为键,用户信息作为值存入Redis
redisService.set(UserConstants.REDIS_USER + accessToken , JSON.toJSONString(user),UserConstants.REDIS_USER_TIME);
return user;
}
此时用户登录成功后,就会返回Token给客户端
6.登录拦截器
AuthenticationInterceptor.java
public class AuthenticationInterceptor implements HandlerInterceptor {
public final static String ACCESS_TOKEN = "ltz-Token";
@Autowired
private LoginService loginService;
@Autowired
private RedisService redisService;
// 在业务处理器处理请求之前被调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 判断接口是否需要登录
LoginRequired methodAnnotation = method.getAnnotation(LoginRequired.class);
// 有 @LoginRequired 注解,需要认证
if (methodAnnotation != null) {
// 判断是否存在令牌信息,如果存在,则允许登录
String accessToken = request.getHeader(ACCESS_TOKEN);
if (null == accessToken) {
throw new Exception("相应的状态码");
} else {
// 从Redis 中查看 token 是否过期
long expire = redisService.getExpire(UserConstants.REDIS_USER + accessToken);
if (expire <= 0 ){
//不存在该用户
throw new Exception("相应的状态码");
}
Claims claims;
try{
claims = TokenUtil.parseJWT(accessToken);
}catch (ExpiredJwtException e){
response.setStatus(401);
throw new Exception("相应的状态码");
}catch (SignatureException se){
response.setStatus(401);
throw new Exception("相应的状态码");
}catch (Exception ee){
response.setStatus(401);
throw new Exception("相应的状态码");
}
User user = new User();
user.setUname(claims.getId());
// 根据用户名查找用户方法
user = loginService.findUserByUname(user);
if (user == null) {
response.setStatus(401);
throw new Exception("相应的状态码");
}
String userJson = redisService.get(UserConstants.REDIS_USER + accessToken);
// 从Redis中获取用户信息
User userA;
userA= JSON.parseObject(userJson, User.class);
if (userJson!=null && userJson!=""&&userA!=null){
// 当前登录用户@CurrentUser
request.setAttribute(UserConstants.CURRENT_USER, userA);
}else {
// 用户信息有问题,不予登录
throw new Exception("相应的状态码");
}
return true;
}
} else {//不需要登录可请求
return true;
}
}
// 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
// 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
CurrentUserMethodArgumentResolver.java
/**
*
* @BelongsPackage: com.jdtaste.jdtastesso.web.intercepter.auth
* @Author: litong
* @CreateTime: 2018-07-04 15:42
* @Description: 自定义参数解析器
* 增加方法注入,将含有 @CurrentUser 注解的方法参数注入当前登录用户
*/
public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
/**
* supportsParameter:用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument。
* @param parameter
* @return
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
System.out.println("----------supportsParameter-----------" + parameter.getParameterType());
//判断是否能转成User 类型
return parameter.getParameterType().isAssignableFrom(User.class)
//是否有CurrentUser注解
&& parameter.hasParameterAnnotation(CurrentUser.class);
}
/**
* resolveArgument:真正用于处理参数分解的方法,返回的Object就是controller方法上的形参对象。
* @param parameter
* @param mavContainer
* @param webRequest
* @param binderFactory
* @return
* @throws Exception
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
System.out.println("--------------resolveArgument-------------" + parameter);
User user = (User) webRequest.getAttribute(UserConstants.CURRENT_USER, RequestAttributes.SCOPE_REQUEST);
if (user != null) {
return user;
}
throw new MissingServletRequestPartException(UserConstants.CURRENT_USER);
}
}
7.配置拦截器
注意:SpringBoot2.0中,WebMvcConfigurerAdapter
已经过时,改为WebMvcConfigurer
。
WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns 用于添加拦截规则
// excludePathPatterns 用户排除拦截
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/*/*");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(currentUserMethodArgumentResolver());
}
@Bean
public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver() {
return new CurrentUserMethodArgumentResolver();
}
/**
* 解决 拦截器中注入bean 失败情况出现
* addArgumentResolvers方法中 添加
* argumentResolvers.add(currentUserMethodArgumentResolver());
*/
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
8.验证方法
在Controller中,添加一个测试接口
添加@LoginRequired
注解,表示,此接口请求时必须有Token
/**
* Token测试接口
* @param user
* @return
*/
@LoginRequired
@GetMapping("/getMessage")
public Map<String,Object> getMessage(@CurrentUser User user){
Map<String,Object> map = null;
System.out.println(user.toString());
map.push("code","1000");
map.push("user",user);
return map;
}