用户认证&授权
1.有状态VS无状态
有状态
服务器端要维护用户的登录状态,通常使用Session Store来存储用户的登录状态
- 优点: 服务端控制力强
- 缺点: 存在中心点,一旦Session Store挂了就全完了,迁移麻烦,服务器端存储数据,加大了服务器压力。
无状态
服务器端不要维护用户的登录状态,在用户登录时给用户颁发一个Token,后期用户访问都要携带Token到服务器进行验证
- 优点: 去中心化,无存储,简单,任意扩容,缩容
- 缺点: 服务器控制力相对弱
2.JWT
什么是JWT
JWT全称Json web token,是一个开放标准(RFC 7519),用来在各方之间安全地传输信息,JWT可被信任和验证,应为它时数字签名。
JWT的组成
- Header(头)
记录令牌类型、签名的算法等:{“alg”:“HS 256”,“typ”:“JWT”} - Payload(有效载荷)
携带一些用户信息:{“id”:1,“username”:“xxx”} - Signature(签名)
防止Token被篡改、确保安全性:计算出来的签名,一个字符串
JWT算法
Token = Base64(Header).Base64(Payload).Base64(Signature)
- 示例:aaaa.bbbb.ccc
Signature = Header指定的签名算法(Base64(Header).Base64(Payload),密钥)
- 示例:HS256(“aaaa.bbbb”,密钥)
3.JWT的使用
pom
文件引入依赖
<!--引入jwt加密工具-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
引入JWT工具类JwtOperator
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
@Component
public class JwtOperator {
/**
* 秘钥
* - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
*/
@Value("${secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
private String secret;
/**
* 有效期,单位秒
* - 默认2周
*/
@Value("${expire-time-in-second:1209600}")
private Long expirationTimeInSecond;
/**
* 从token中获取claim
*
* @param token token
* @return claim
*/
public Claims getClaimsFromToken(String token) {
try {
return Jwts.parser()
.setSigningKey(this.secret.getBytes())
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
log.error("token解析错误", e);
throw new IllegalArgumentException("Token invalided.");
}
}
/**
* 获取token的过期时间
*
* @param token token
* @return 过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token)
.getExpiration();
}
/**
* 判断token是否过期
*
* @param token token
* @return 已过期返回true,未过期返回false
*/
private Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 计算token的过期时间
*
* @return 过期时间
*/
public Date getExpirationTime() {
return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
}
/**
* 为指定用户生成token
*
* @param claims 用户信息
* @return token
*/
public String generateToken(Map<String, Object> claims) {
Date createdTime = new Date();
Date expirationTime = this.getExpirationTime();
byte[] keyBytes = secret.getBytes();
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(createdTime)
.setExpiration(expirationTime)
// 你也可以改用你喜欢的算法
// 支持的算法详见:https://github.com/jwtk/jjwt#features
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 判断token是否非法
*
* @param token token
* @return 未过期返回true,否则返回false
*/
public Boolean validateToken(String token) {
return !isTokenExpired(token);
}
}
示例代码
/**
* 模拟登录生成token
* @return
*/
@GetMapping("/gen-token")
public String genToken(){
Map<String,Object> userInfo = new HashMap<>(3);
userInfo.put("id", 1);
userInfo.put("name","空空");
userInfo.put("role","user");
return this.jwtOperator.generateToken(userInfo);
}
4.使用AOP实现登录状态检查
pom
文件引入依赖
<!--添加spring aop的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
新建一个注解
public @interface CheckLogin {
}
新建一个切面CheckLoginAspect
用于CheckLogin
注解的校验
示例:假设用户请求必须携带一个名为X-Token
的Token参数作为校验是否已经登录
/**
* @author ZL
* @date 2020/2/27 16:57
*/
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class CheckLoginAspect {
private final JwtOperator jwtOperator;
@Around("@annotation(com.itkk.usercenter.auth.CheckLogin)")
public Object checkLogin(ProceedingJoinPoint point){
try {
//1.从header里面获取token
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes =(ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("X-Token");
//2.交易Token的合法性和有效性
Boolean isValid = this.jwtOperator.validateToken(token);
if(!isValid){
throw new SecurityException("Token校验不合法");
}
//3.交易成功的话将用户信息设置到request的attribute里面
Claims claims = jwtOperator.getClaimsFromToken(token);
request.setAttribute("id",claims.get("id"));
request.setAttribute("name",claims.get("name"));
request.setAttribute("role",claims.get("role"));
return point.proceed();
} catch (Throwable throwable) {
throw new SecurityException("Token不合法");
}
}
}
在需要校验的请求上添加新建的登录校验注解CheckLogin
/**
* @CheckLogin注解用于校验用户是否登录
* @param id
* @return
*/
@RequestMapping("/{id}")
@CheckLogin
public User findById(@PathVariable Integer id){
log.info("被请求了");
return this.userService.findById(id);
}
5.Feign实现Token传递
注意: 推荐使用方法二,对代码侵入性弱
方法一:使用@RequestHeader
传参
Controller里的方法添加@RequestHeader(“X-Token”) 用来获取请求里的Token
@GetMapping("/{id}")
@CheckLogin
public ShareDTO getById(@PathVariable Integer id
,@RequestHeader("X-Token") String token) {
ShareDTO sharesDTO = this.shareService.findById(id,token);
return sharesDTO;
}
UserCenterFeignClient接口添加@RequestHeader(“X-Token”) 用来传递Token
public interface UserCenterFeignClient {
@GetMapping("/users/{id}")
UserDTO findById(@PathVariable(value="id") Integer id
,@RequestHeader("X-Token") String token);
}
方法二:使用拦截器传递Token
新建TokenRelayRequestInteceptor
类实现feign
的RequestInterceptor
接口
/**
* @author ZL
* @date 2020/2/28 11:45
*/
public class TokenRelayRequestInteceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
//1.获取Token
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes =(ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("X-Token");
//2.传递Token
if(StringUtils.isNotBlank(token)){
template.header("X-Token",token);
}
}
}
Feign添加全局配置requestInterceptors
feign:
sentinel:
# 为feign整合sentinel
enabled: true
client:
config:
#全局配置
default:
loggerLevel: full
requestInterceptors:
- com.itkk.contentcenter.interceptor.TokenRelayRequestInteceptor
请求通过
6.RestTemplater实现Token传递
方法一:使用exchange()
传递Token
@GetMapping("/tokenRelay/{userId}")
public ResponseEntity<UserDTO> tokenRelay(@PathVariable Integer userId, HttpServletRequest request) {
String token = request.getHeader("X-Token");
HttpHeaders headers = new HttpHeaders();
headers.add("X-Token", token);
return this.restTemplate
.exchange(
"http://user-center-kk/users/{userId}",
HttpMethod.GET,
new HttpEntity<>(headers),
UserDTO.class,
userId
);
}
方法二:使用ClientHttpRequestInterceptor
传递Token
新建一个TestRestTemplateTokenRelayInterceptor
类实现ClientHttpRequestInterceptor
接口
public class TestRestTemplateTokenRelayInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest httpRequest = attributes.getRequest();
String token = httpRequest.getHeader("X-Token");
HttpHeaders headers = request.getHeaders();
headers.add("X-Token", token);
// 保证请求继续执行
return execution.execute(request, body);
}
}
修改RestTemplate的Bean配置
@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(
Collections.singletonList(
new TestRestTemplateTokenRelayInterceptor()
)
);
return restTemplate;
}
7.授权(角色授权)
新增CheckAuthorization
认证校验注解
/**
* @author ZL
* @date 2020/2/28 14:43
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthorization {
String value();
}
修改CheckLoginAspect
类为AuthAspect
,并添加授权认证的代码
/**
* @author ZL
* @date 2020/2/27 16:57
*/
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AuthAspect {
private final JwtOperator jwtOperator;
@Around("@annotation(com.itkk.contentcenter.auth.CheckLogin)")
public Object checkLogin(ProceedingJoinPoint point) throws Throwable {
checkToken();
return point.proceed();
}
private void checkToken() {
try {
// 1. 从header里面获取token
HttpServletRequest request = getHttpServletRequest();
String token = request.getHeader("X-Token");
// 2. 校验token是否合法&是否过期;如果不合法或已过期直接抛异常;如果合法放行
Boolean isValid = jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("Token不合法!");
}
// 3. 如果校验成功,那么就将用户的信息设置到request的attribute里面
Claims claims = jwtOperator.getClaimsFromToken(token);
request.setAttribute("id", claims.get("id"));
request.setAttribute("wxNickname", claims.get("wxNickname"));
request.setAttribute("role", claims.get("role"));
} catch (Throwable throwable) {
throw new SecurityException("Token不合法");
}
}
private HttpServletRequest getHttpServletRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
return attributes.getRequest();
}
@Around("@annotation(com.itkk.contentcenter.auth.CheckAuthorization)")
public Object checkAuthorization(ProceedingJoinPoint point) throws Throwable {
try {
// 1. 验证token是否合法;
this.checkToken();
// 2. 验证用户角色是否匹配
HttpServletRequest request = getHttpServletRequest();
String role = (String) request.getAttribute("role");
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
CheckAuthorization annotation = method.getAnnotation(CheckAuthorization.class);
String value = annotation.value();
if (!Objects.equals(role, value)) {
throw new SecurityException("用户无权访问!");
}
} catch (Throwable throwable) {
throw new SecurityException("用户无权访问!", throwable);
}
return point.proceed();
}
}
需要授权认证的方法上添加@CheckAuthorization("角色名称")
注解
@PutMapping("/audit/{id}")
@CheckAuthorization("admin")
public Share auditById(@PathVariable Integer id,@RequestBody ShareAuditDTO shareAuditDTO){
//TODO 认证授权
return this.shareService.auditById(id,shareAuditDTO);
}
未完待续!!!
上一篇: 练习Spring Cloud Alibaba —— 6.
下一篇: 练习Spring Cloud Alibaba —— 8.