SpringBoot系列(十八):token认证

我们先来讲一下一般系统的权限控制问题。

控制权限的方法,一般分为三种:

  • 角色具有某些权限
  • 用户具有某些权限
  • 用户具有某些角色

用运营成本来说,建议用户不具有权限;通过角色赋予权限,然后通过角色赋予用户。

权限需要思考的问题:
1、权限粒度:不同权限,查询不同的字段
2、角色能否创建子角色;角色被回收权限时子角色权限问题;角色能否修改子子角色权限等问题。

权限校验分为两种:登录获得令牌验证所持令牌

用户登录的方式有很多

  • 账号密码
  • 邮箱与验证码
  • 单点登录,获取token

票据方式:

1、Cookie
2、令牌 JWT:拥有uid、其他信息和时效性
令牌的时效性,我们需要思考一个问题?
例如定义了一个令牌是2小时失效,如果用户使用3小时,令牌会在中途失效,导致需要重新登录,如何解决这个问题呢?
使用双令牌机制,access_token和refresh_token

1、登录获得令牌

验证的逻辑顺序

  • controller接收参数
  • model:返回用户信息
  • service:验证用户信息
  • service :生成Token

1.1、创建一个TokenController,获取Token

在api中创建TokenController

package gani.vankek3api.api.v1;


import gani.vankek3api.dto.PersonDTO;
import gani.vankek3api.dto.TokenGetDTO;
import gani.vankek3api.exception.NotFoundException;
import gani.vankek3api.service.VankeAuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

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

import static gani.vankek3api.core.enumeration.LoginType.USER_VANKE;

@RestController
@RequestMapping("/api/v1/token/")
public class TokenController {

    @Autowired
    private VankeAuthenticationService vankeAuthenticationService;

    @PostMapping("/gettoken")
    public Map<String, String> getToken(@RequestBody @Validated TokenGetDTO userData) {
        Map<String, String> map = new HashMap<>();
        String token = null;
        switch (userData.getType()) {
            case USER_VANKE:
                token = vankeAuthenticationService.getToken(userData.getAccount(),userData.getPassword());
                break;
            case USER_Email:
                break;
            default:
                throw new NotFoundException(10003);
        }
        map.put("token", token);
        return map;
    }
}

1.2、创建TokenGetDTO接收参数

在的dto下创建一个 TokenGetDTO 类

package gani.vankek3api.dto;


import gani.vankek3api.core.enumeration.LoginType;
import gani.vankek3api.dto.validators.TokenPassword;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;

@Getter
@Builder
//@NoArgsConstructor
@Setter
public class TokenGetDTO {
    @NotBlank(message = "account不允许为空")
    private String account;

    @TokenPassword(max=10, message = "{token.password}")
    private String password;

    private LoginType type;
}

如果@NotBlank报错的话,就在pom.xml中引入

        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>2.0.2</version>
        </dependency>

1.3、创建自定义注解的方式 @TokenPassword

在DTO下面创建一个package,并命名为validators
创建一个Annotation,命名为TokenPassword

package gani.vankek3api.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 gani.vankek3api.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) {
        if(StringUtils.isEmpty(s)){
            return true;
        }
        return s.length() >= this.min && s.length() <= this.max;
    }
}

1.4、创建LoginType枚举

在core中创建一个package,并命名为enumeration
然后再创建一个枚举Enum,并命名为LoginType

package gani.vankek3api.core.enumeration;

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

    private Integer value;

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

1.5、创建用户认证Service: AuthenticationService

接口

package gani.vankek3api.service;

public interface VankeAuthenticationService {
    String getToken(String InAccount,String InPassword);
}

实现类

package gani.vankek3api.service;

import gani.vankek3api.exception.ParameterException;
import gani.vankek3api.util.JwtToken;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class VankeAuthenticationServiceImpl implements VankeAuthenticationService{
    @Value("${vanke.account}")
    private String VankeAccount;
    @Value("${vanke.password}")
    private String VankePassword;

    public String getToken(String InAccount,String InPassword) {
        String Token = "";
        if (InAccount == null || InPassword == null){
            throw new ParameterException(20004);
        }
        if(InAccount.equals(this.VankeAccount) && InPassword.equals(this.VankePassword)){
            /*定义该用户等级为8,真正的业务逻辑是查询数据库获取uid和用户等级*/
            Integer scope = 8;
            Long Uid = 1L;
            Token = JwtToken.makeToken(Uid,scope);
        }else{
            throw new ParameterException(20004);
        }
        return Token;
    }
}

1.6、创建JWT令牌

1.6.1 引入依赖

首先引入auth0,修改pom.xml。

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

1.6.2 修改配置项,

vanke:
  account: EnterAccount
  password: EnterPpassword
  security:
    jwt-key: vankegani
    token-expired-in: 86400000

1.6.3 创建公共类 JwtToken

在util中创建一个类,并命名为JwtToken

注意Algorithm 引用的是auth0

记得修改参数

package gani.vankek3api.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("${vanke.security.jwt-key}")
    public void setJwtKey(String jwtKey) {
        JwtToken.jwtKey = jwtKey;
    }

    @Value("${vanke.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;
    }
}

定义参数:

JAVA、基础技术、技术与框架SpringBoot系列(十八):token认证插图

测试获取token

JAVA、基础技术、技术与框架SpringBoot系列(十八):token认证插图1

2、验证Token

验证案例,我们先从逻辑入手获取到请求的token

  • 获取到请求的token
  • 验证是否token
  • 读取token里面的数据
  • 读取api中@ScopeLevel level [自定义注解]
  • 判断scope和@ScopeLevel,返回Boolean

2.1、UserService

2.1.1创建用户model

model/user

package gani.vankek3api.model;

import lombok.*;

import javax.persistence.Entity;

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
public class User {
    private Long id;
    private String openid;

    private String nickname;

    private String email;

    private String mobile;
}

2.1.2service/UserService【接口】

package gani.vankek3api.service;

import gani.vankek3api.model.User;

public interface UserService {
    public User getUserInfoById(Long uid);
}

2.1.3service/UserServiceImpl

【Service】创建根据ID获取用户信息的方法

package gani.vankek3api.service;

import gani.vankek3api.model.User;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements  UserService{

    public User getUserInfoById(Long uid){
        User Userinfo = User.builder()
                        .id(1L)
                        .nickname("Vanke")
                        .build();
        return Userinfo;
    }
}

2.2在core下创建LocalUser的类

package gani.vankek3api.core;

import gani.vankek3api.model.User;

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

public class LocalUser {
    private static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();

    public static void set(User user, Integer scope) {
        Map<String, Object> map = new HashMap<>();
        map.put("user", user);
        map.put("scope", scope);
        LocalUser.threadLocal.set(map);
    }

    public static void clear() {
        LocalUser.threadLocal.remove();
    }

    public static User getUser() {
        Map<String, Object> map = LocalUser.threadLocal.get();
        User user = (User)map.get("user");
        return user;
    }

    public static Integer getScope() {
        Map<String, Object> map = LocalUser.threadLocal.get();
        Integer scope = (Integer)map.get("scope");
        return scope;
    }
}

2.3定义@ScopeLevel

在core下创建package,并命名为interceptors

在core/interceptors 下创建一个类,并命名为ScopeLevel

package gani.vankek3api.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)
/* 默认等级是4 */
public @interface ScopeLevel {
    int value() default 4;
}

2.4创建拦截器

在core/interceptors 下创建一个类,并命名为PermissionInterceptor

因为我的SpringBoot版本问题,HandlerInterceptorAdapter 已经过时,需要增加@Deprecated

也可以不用继承的方式,使用实现接口的方式

public class PermissionInterceptor implements HandlerInterceptor {
}
JAVA、基础技术、技术与框架SpringBoot系列(十八):token认证插图2

已废弃的方法:

package gani.vankek3api.core.interceptors;

import com.auth0.jwt.interfaces.Claim;
import gani.vankek3api.core.LocalUser;
import gani.vankek3api.exception.ForbiddenException;
import gani.vankek3api.exception.UnAuthenticatedException;
import gani.vankek3api.model.User;
import gani.vankek3api.service.UserService;
import gani.vankek3api.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;

@Deprecated
public class PermissionInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private UserService userService;

    public PermissionInterceptor() {
        super();
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Optional<ScopeLevel> scopeLevel = this.getScopeLevel(handler);
        if (!scopeLevel.isPresent()) {
            return true;
        }

        String bearerToken = request.getHeader("Authorization");
        if(bearerToken == null){
            throw new UnAuthenticatedException(10001);
        }


        if (!bearerToken.startsWith("Bearer")) {
            throw new UnAuthenticatedException(10004);
        }
        String tokens[] = bearerToken.split(" ");
        if (!(tokens.length == 2)) {
            throw new UnAuthenticatedException(10004);
        }
        String token = tokens[1];
        Optional<Map<String, Claim>> optionalMap = JwtToken.getClaims(token);
        /*解析token,获得之前生成token的信息
        * 在service/VankeAuthenticationServiceImpl这里的make token传的参数
        * */
        Map<String, Claim> map = optionalMap
                .orElseThrow(() -> new UnAuthenticatedException(10004));

        boolean valid = this.hasPermission(scopeLevel.get(), map);
        if(valid){
            this.setToThreadLocal(map);
        }
        return valid;
    }

    private void setToThreadLocal(Map<String,Claim> map) {
        Long uid = map.get("uid").asLong();
        Integer scope = map.get("scope").asInt();
        User user = this.userService.getUserInfoById(uid);
        LocalUser.set(user, scope);
    }
    /* 判断请求的token权限与ScopeLevel注解等级对比 */
    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);
    }

    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();
    }

}

最新方法

package gani.vankek3api.core.interceptors;

import com.auth0.jwt.interfaces.Claim;
import gani.vankek3api.core.LocalUser;
import gani.vankek3api.exception.ForbiddenException;
import gani.vankek3api.exception.UnAuthenticatedException;
import gani.vankek3api.model.User;
import gani.vankek3api.service.UserService;
import gani.vankek3api.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.AsyncHandlerInterceptor;
import org.springframework.web.servlet.HandlerInterceptor;
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 implements HandlerInterceptor {

    @Autowired
    private UserService userService;



    public PermissionInterceptor() {
        super();
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Optional<ScopeLevel> scopeLevel = this.getScopeLevel(handler);
        if (!scopeLevel.isPresent()) {
            return true;
        }

        String bearerToken = request.getHeader("Authorization");
        if(bearerToken == null){
            throw new UnAuthenticatedException(10001);
        }


        if (!bearerToken.startsWith("Bearer")) {
            throw new UnAuthenticatedException(10004);
        }
        String tokens[] = bearerToken.split(" ");
        if (!(tokens.length == 2)) {
            throw new UnAuthenticatedException(10004);
        }
        String token = tokens[1];
        Optional<Map<String, Claim>> optionalMap = JwtToken.getClaims(token);
        /*解析token,获得之前生成token的信息
        * 在service/VankeAuthenticationServiceImpl这里的make token传的参数
        * */
        Map<String, Claim> map = optionalMap
                .orElseThrow(() -> new UnAuthenticatedException(10004));

        boolean valid = this.hasPermission(scopeLevel.get(), map);
        if(valid){
            this.setToThreadLocal(map);
        }
        return valid;
    }

    private void setToThreadLocal(Map<String,Claim> map) {
        Long uid = map.get("uid").asLong();
        Integer scope = map.get("scope").asInt();
        User user = this.userService.getUserInfoById(uid);
        LocalUser.set(user, scope);
    }

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

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

    /* 判断请求的token权限与ScopeLevel注解等级对比 */
    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;
    }



    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();
    }

}

2.5、注册拦截器

在core/configuration下面创建一个类,并命名IntterceptorConfiguration

废弃方法

package gani.vankek3api.core.configuration;

import gani.vankek3api.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;
@Deprecated
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {

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

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

最新方法

package gani.vankek3api.core.configuration;

import gani.vankek3api.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认证的地方加入注解。在传参的时候,header增加Authorization:tokenValue

JAVA、基础技术、技术与框架SpringBoot系列(十八):token认证插图3

扩展学习:拦截

filter:servlet
interceptor aop:spring
如果spring中中心存在以上三个拦截方法时,顺序
filter interceptor apo -> interceptor -> filter
但建议使用interceptor

在Override的时候,有三个方法:preHandle、postHandle、afterCompletion

  • preHandle :请求在进入controller之前的时候,回调
  • postHandle :渲染页面时,可以修改ModelAndView
  • afterCompletion 用于清楚资源