SpringBoot2.0 添加Token、Redis

版权声明: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;
 }

可以在PostMan中,模拟请求该接口。

注意在Head中,添加请求头。

key为:ltz-Token

value为:生成的Token

Token解析成功后,会从Redis中,以token为键,取用户信息。然后注入到有@CurrentUser注解的User中。

猜你喜欢

转载自blog.csdn.net/LitongZero/article/details/88177716