我们先来讲一下一般系统的权限控制问题。
控制权限的方法,一般分为三种:
- 角色具有某些权限
- 用户具有某些权限
- 用户具有某些角色
用运营成本来说,建议用户不具有权限;通过角色赋予权限,然后通过角色赋予用户。
权限需要思考的问题:
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;
}
}
定义参数:
测试获取token
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 {
}
已废弃的方法:
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
扩展学习:拦截
filter:servlet
interceptor aop:spring
如果spring中中心存在以上三个拦截方法时,顺序
filter interceptor apo -> interceptor -> filter
但建议使用interceptor
在Override的时候,有三个方法:preHandle、postHandle、afterCompletion
- preHandle :请求在进入controller之前的时候,回调
- postHandle :渲染页面时,可以修改ModelAndView
- afterCompletion 用于清楚资源