TokenGetDTO–基于jwt登录校验实体

1.TokenGetDTO:

package com.zhqx.missyou.dto;

import com.zhqx.missyou.core.enumeration.LoginType;
import com.zhqx.missyou.dto.validators.TokenPassword;
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;

/**
 * 通用用户登录dto
 * 在不同登录方式中
 * account和password代表的内容不同
 */
@Getter
@Setter
public class TokenGetDTO {
    @NotBlank(message = "account不允许为空")
    private String account;
    @TokenPassword(max=30, message = "{token.password}")
    private String password;

    private LoginType type;
}

2.@TokenPassword,自定义密码校验注解:

TokenPassword:

package com.zhqx.missyou.dto.validators;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
@Constraint(validatedBy = TokenPasswordValidator.class )
public @interface TokenPassword {
    String message() default "字段不符合要求";

    int min() default 6;

    int max() default 32;

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

TokenPasswordValidator:

package com.zhqx.missyou.dto.validators;

import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class TokenPasswordValidator implements ConstraintValidator<TokenPassword, String> {
    private Integer min;
    private Integer max;

    @Override
    public void initialize(TokenPassword constraintAnnotation) {
        this.min = constraintAnnotation.min();
        this.max = constraintAnnotation.max();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
		//StringUtils.isEmpty() 2.5.2废弃 -- 使用 !StringUtils.hasText(s)做条件
        if(StringUtils.isEmpty(s)){
			//小程序登录不需要password,所以直接返回true
            return true;
        }
        return s.length() >= this.min && s.length() <= this.max;
    }
}

3.LoginType,登录方式枚举:

package com.zhqx.missyou.core.enumeration;

public enum  LoginType {
    USER_WX(0, "微信登录"),
    USER_EMAIL(1, "邮箱登录");

    private Integer value;

    LoginType(Integer value, String description) {
        this.value = value;
    }
}

在resources目录下的ValidationMessages.properties配置密码校验信息:

id.positive = id必须是正整数
token.password = password不符合规范:当前值是${validatedValue};最大值应该是{max},最小值应该是{min}

获取微信小程序登录用户

微信小程序调用wx.login方法.

wx.login({
  success: (res) => {
    if (res.code) {
      wx.request({
        url: 'http://localhost:8080/v1/token',
        method: 'POST',
        data: {
          account: res.code,
          type: 0
        },
        success: (res) => {
          console.log(res.data)
          const code = res.statusCode.toString()
          if (code.startsWith('2')) {
            wx.setStorageSync('token', res.data.token)
          }
        }
      })
    }
  }
})

在配置文件application.yml配置微信appid等信息:

wx:
  appid: xxx
  appsecret: xxx
  code2session: https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code

WxAuthenticationServiceImpl类:

package com.zhqx.missyou.service.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhqx.missyou.service.WxAuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;

@Service
public class WxAuthenticationServiceImpl implements WxAuthenticationService {

    @Autowired
    private ObjectMapper mapper;

    @Value("${wx.code2session}")
    private String code2SessionUrl;
    @Value("${wx.appid}")
    private String appid;
    @Value("${wx.appsecret}")
    private String appsecret;

    @Override
    public String code2Session(String code) {
        String url = MessageFormat.format(this.code2SessionUrl, this.appid, this.appsecret, code);
        RestTemplate rest = new RestTemplate();
        Map<String, Object> session = new HashMap<>();
        String sessionText = rest.getForObject(url, String.class);
        try {
            session = mapper.readValue(sessionText, Map.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return this.registerUser(session);
    }

	@Override
	public String registerUser(Map<String, Object> session) {
    	return null;
	}

}

TokenController类:

package com.zhqx.missyou.api.v1;

import com.zhqx.missyou.dto.TokenGetDTO;
import com.zhqx.missyou.exception.http.NotFoundException;
import com.zhqx.missyou.service.WxAuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RequestMapping(value = "token")
@RestController
public class TokenController {

    @Autowired
    private WxAuthenticationService wxAuthenticationService;

    @PostMapping("")
    public Map<String, String> getToken(@RequestBody @Validated TokenGetDTO userData) {
        Map<String, String> map = new HashMap<>();
        String token = null;
        switch (userData.getType()) {
            case USER_WX:
                token = wxAuthenticationService.code2Session(userData.getAccount());
                break;
            case USER_EMAIL:
                break;
            default:
                throw new NotFoundException(10003);
        }
        map.put("token", token);
        return map;
    }
}

注册小程序用户或发放jwt

User类

package com.zhqx.missyou.model;

import com.zhqx.missyou.util.MapAndJson;
import lombok.*;
import org.hibernate.annotations.Where;

import javax.persistence.*;
import java.util.Map;

import static javax.persistence.GenerationType.IDENTITY;

@NoArgsConstructor
@AllArgsConstructor
@Entity
@Getter
@Setter
@Where(clause = "delete_time is null")
public class User extends BaseEntity {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;
    private String openid;

    private String nickname;

    private String email;

    private String mobile;

    private String password;

    private Long unifyUid;

    @Convert(converter = MapAndJson.class)
    private Map<String, Object> wxProfile;

}

UserRepository类:

package com.zhqx.missyou.repository;

import com.zhqx.missyou.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByEmail(String email);
    Optional<User> findByOpenid(String openid);
    User findFirstById(Long id);
    User findByUnifyUid(Long uuid);
}

添加jwt库:

    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.8.1</version>
    </dependency>

在配置文件resources/config/application-dev.yml中配置jwt相关配置项:

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/sleeve?characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: root
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

zhqx:
  security:
    jwt-key: marshu
    token-expired-in: 86400000

JwtToken类:

package com.zhqx.missyou.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class JwtToken {

    private static String jwtKey;
    private static Integer expiredTimeIn;
    //默认权限级别
    private static Integer defaultScope = 8;

    @Value("${zhqx.security.jwt-key}")
    public void setJwtKey(String jwtKey) {
        JwtToken.jwtKey = jwtKey;
    }

    @Value("${zhqx.security.token-expired-in}")
    public void setExpiredTimeIn(Integer expiredTimeIn) {
        JwtToken.expiredTimeIn = expiredTimeIn;
    }

    public static Optional<Map<String, Claim>> getClaims(String token) {
        DecodedJWT decodedJWT;
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            decodedJWT = jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
            return Optional.empty();
        }
        return Optional.of(decodedJWT.getClaims());
    }

    public static Boolean verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(token);
        } catch (JWTVerificationException e) {
            return false;
        }
        return true;
    }


    public static String makeToken(Long uid, Integer scope) {
        return JwtToken.getToken(uid, scope);
    }

    public static String makeToken(Long uid) {
        return JwtToken.getToken(uid, JwtToken.defaultScope);
    }

    private static String getToken(Long uid, Integer scope) {
        Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);
        //计算过期时间
        Map<String,Date> map = JwtToken.calculateExpiredIssues();
        return JWT.create()
                .withClaim("uid", uid)
                .withClaim("scope", scope)
                .withExpiresAt(map.get("expiredTime"))//过期时间
                .withIssuedAt(map.get("now"))//签发时间
                .sign(algorithm);
    }

    //当前时间+过期时间 就是令牌最终失效时间
    private static Map<String, Date> calculateExpiredIssues() {
        Map<String, Date> map = new HashMap<>();
        Calendar calendar = Calendar.getInstance();
        Date now = calendar.getTime();
        calendar.add(Calendar.SECOND, JwtToken.expiredTimeIn);
        map.put("now", now);
        map.put("expiredTime", calendar.getTime());
        return map;
    }
}

定义ParameterException:

package com.zhqx.missyou.exception.http;

public class ParameterException extends HttpException {
    public ParameterException(int code){
        this.code = code;
        this.httpStatusCode = 400;
    }
}

在resources/config/exception-code.properties中配置ParameterException异常信息:

zhqx.codes[9999] = 服务器未知异常O(∩_∩)O哈哈~

zhqx.codes[10000] = 通用异常
zhqx.codes[10001] = 通用参数异常
zhqx.codes[10003] = 没有找到合适的登陆处理方法

zhqx.codes[20004] = 获取用户wx openid失败

zhqx.codes[30003] = 商品信息不存在
zhqx.codes[30006] = Spu类的资源不存在

修改WxAuthenticationServiceImpl类:

package com.zhqx.missyou.service.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhqx.missyou.exception.http.ParameterException;
import com.zhqx.missyou.model.User;
import com.zhqx.missyou.repository.UserRepository;
import com.zhqx.missyou.service.WxAuthenticationService;
import com.zhqx.missyou.util.JwtToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Service
public class WxAuthenticationServiceImpl implements WxAuthenticationService {

    @Autowired
    private ObjectMapper mapper;

    @Autowired
    private UserRepository userRepository;

    @Value("${wx.code2session}")
    private String code2SessionUrl;
    @Value("${wx.appid}")
    private String appid;
    @Value("${wx.appsecret}")
    private String appsecret;

    public String code2Session(String code) {
        String url = MessageFormat.format(this.code2SessionUrl, this.appid, this.appsecret, code);
        RestTemplate rest = new RestTemplate();
        Map<String, Object> session = new HashMap<>();
        String sessionText = rest.getForObject(url, String.class);
        try {
            session = mapper.readValue(sessionText, Map.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return this.registerUser(session);
    }

    private String registerUser(Map<String, Object> session) {
        String openid = (String)session.get("openid");
        if (openid == null){
            throw new ParameterException(20004);
        }
        Optional<User> userOptional = this.userRepository.findByOpenid(openid);
        if(userOptional.isPresent()){//用户存在时
            //TODO 数字等级
            return JwtToken.makeToken(userOptional.get().getId());
        }
        User user = new User();
        user.setOpenid(openid);
        userRepository.save(user);
        Long uid = user.getId();
        return JwtToken.makeToken(uid);
    }
}

jwt-token验证

因为在之前的JWT中,已经预先引入了scope的概念(访问权限),自定义权限注解@ScopeLevel:

package com.zhqx.missyou.core.interceptors;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ScopeLevel {
    int value() default 4;
}

定义拦截器,拦截当前访问API用户token以及scope.

package com.zhqx.missyou.core.interceptors;

import com.auth0.jwt.interfaces.Claim;
import com.zhqx.missyou.exception.http.ForbiddenException;
import com.zhqx.missyou.exception.http.UnAuthenticatedException;
import com.zhqx.missyou.model.User;
import com.zhqx.missyou.service.UserService;
import com.zhqx.missyou.util.JwtToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.Optional;

public class PermissionInterceptor extends HandlerInterceptorAdapter {

    public PermissionInterceptor() {
        super();
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Optional<ScopeLevel> scopeLevel = this.getScopeLevel(handler);
        if (!scopeLevel.isPresent()) {//当前API访问没有权限注解,属于公开API
            return true;
        }
        //获取请求头中携带的token信息 --规范上 前端会为token进行bearer加密
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.isEmpty(bearerToken)) {
            throw new UnAuthenticatedException(10004);
        }
        if (!bearerToken.startsWith("Bearer")) {//不是标准的token
            throw new UnAuthenticatedException(10004);
        }
        String tokens[] = bearerToken.split(" ");
        if (!(tokens.length == 2)) {//为了避免有Bearer,但是没有token值的情况,对tokens长度进行判断
            throw new UnAuthenticatedException(10004);
        }
        String token = tokens[1];
        Optional<Map<String, Claim>> optionalMap = JwtToken.getClaims(token);
        Map<String, Claim> map = optionalMap
                .orElseThrow(() -> new UnAuthenticatedException(10004));
        //比较当前token信息中的scopeLevel与接口中的scopeLevel
        boolean valid = this.hasPermission(scopeLevel.get(), map);
        return valid;
    }

    private boolean hasPermission(ScopeLevel scopeLevel, Map<String, Claim> map) {
        Integer level = scopeLevel.value();
        Integer scope = map.get("scope").asInt();
        if (level > scope) {
            throw new ForbiddenException(10005);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }

    //获取访问方法的@ScopeLevel注解
    private Optional<ScopeLevel> getScopeLevel(Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            ScopeLevel scopeLevel = handlerMethod.getMethod().getAnnotation(ScopeLevel.class);
            if (scopeLevel == null) {
                return Optional.empty();
            }
            return Optional.of(scopeLevel);
        }
        return Optional.empty();
    }

}

定义InterceptorConfiguration,用来注册拦截器

package com.zhqx.missyou.core.configuration;

import com.zhqx.missyou.core.interceptors.PermissionInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {

    @Bean
    public HandlerInterceptor getPermissionInterceptor() {
        return new PermissionInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.getPermissionInterceptor());
    }
}

增加token验证API

package com.zhqx.missyou.api.v1;

import com.zhqx.missyou.dto.TokenDTO;
import com.zhqx.missyou.dto.TokenGetDTO;
import com.zhqx.missyou.exception.http.NotFoundException;
import com.zhqx.missyou.service.WxAuthenticationService;
import com.zhqx.missyou.util.JwtToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RequestMapping(value = "token")
@RestController
public class TokenController {

    @Autowired
    private WxAuthenticationService wxAuthenticationService;

    @PostMapping("")
    public Map<String, String> getToken(@RequestBody @Validated TokenGetDTO userData) {
        Map<String, String> map = new HashMap<>();
        String token = null;
        switch (userData.getType()) {
            case USER_WX:
                token = wxAuthenticationService.code2Session(userData.getAccount());
                break;
            case USER_EMAIL:
                break;
            default:
                throw new NotFoundException(10003);
        }
        map.put("token", token);
        return map;
    }

    @PostMapping("/verify")
    public Map<String, Boolean> verify(@RequestBody TokenDTO token) {
        Map<String, Boolean> map = new HashMap<>();
        Boolean valid = JwtToken.verifyToken(token.getToken());
        map.put("is_valid", valid);
        return map;
    }
}

UserServiceImpl类:

package com.zhqx.missyou.service.impl;

import com.zhqx.missyou.model.User;
import com.zhqx.missyou.repository.UserRepository;
import com.zhqx.missyou.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public User getUserById(Long id) {
        return userRepository.findFirstById(id);
    }

    @Override
    public User createDevUser(Long uid) {
        User newUser = new User();
        newUser.setUnifyUid(uid);
        userRepository.save(newUser);
        return newUser;
    }

    @Override
    public User getUserByUnifyUid(Long uuid) {
        return userRepository.findByUnifyUid(uuid);
    }
}

前端请求需要携带令牌:

前端应该对获取的token进行处理:

  _getBearerToken(){
    const token = wx.getStorageSync('token')
    return 'Bearer ' + token
  }

前端请求时需要携带token:

  onTestInterceptor(){
    wx.request({
      url: 'http://localhost:8080/v1/banner/name/b-1',
      method: 'GET',
      header:{
        'Authorization': this._getBearerToken()
      },
      success: res => {
        console.log(res.data)
      }
    })
  },
原文链接: https://marshucheng1.github.io/2021/02/21/springboot-qiyue-7/