微服务登录分为有状态以及无状态登录方法
有状态登录方法依赖Session,将登录状态信息放置在Session中,并使用一个Session Store存储
服务器端不用去存储session状态,不会出现上述的负载均衡后服务器登录失效问题
优缺点对比
先在认证授权中心登录,登陆成功,颁发Token,用户携带Token去访问微服务
每个微服务都可以解密Token,
需要防止密钥泄露
优点:实现不复杂,降低了网关的复杂度
缺点:密钥泄露是软肋
Role-based access control (RBAC模型)
我们选择最常用的RBAC角色权限控制模型作为DEMO展示
Json web token
JWT组成
不能把敏感信息放在Payload中
公式
http://www.imooc.com/article/290892
加依赖
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.10.7version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.10.7version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.10.7version>
<scope>runtimescope>
dependency>
JWT工具类,可以生成Token,以及判断Token是否合法
package com.example.gateway.utils;
import com.alibaba.nacos.client.identify.Base64;
import com.google.common.collect.Maps;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
@Component
public class JwtOperator {
/**
* 秘钥
* - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
*/
@Value("${jwt.secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
private String secret;
/**
* 有效期,单位秒
* - 默认2周
*/
@Value("${jwt.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 过期时间
*/
private 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);
}
// 测试方法
public static void main(String[] args) {
// 1. 初始化
JwtOperator jwtOperator = new JwtOperator();
jwtOperator.expirationTimeInSecond = 1209600L;
jwtOperator.secret = "aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttr";
// 2.设置用户信息
HashMap<String, Object> objectObjectHashMap = Maps.newHashMap();
objectObjectHashMap.put("id", "1");
// 测试1: 生成token
String token = jwtOperator.generateToken(objectObjectHashMap);
// 会生成类似该字符串的内容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ
System.out.println(token);
// 将我改成上面生成的token!!!
String someToken = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1OTQ5MDMwOTEsImV4cCI6MTIyNTU1MDAzMDkwfQ.a3vyNQz4nbgOo5wB-_69wscmfvQfBgkvm_Q8H_IkTqc";
// 测试2: 如果能token合法且未过期,返回true
Boolean validateToken = jwtOperator.validateToken(someToken);
System.out.println(validateToken);
//
// 测试3: 获取用户信息
Claims claims = jwtOperator.getClaimsFromToken(someToken);
System.out.println(claims);
//
// 将我改成你生成的token的第一段(以.为边界)
String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";
// 测试4: 解密Header
byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
System.out.println(new String(header));
// 将我改成你生成的token的第二段(以.为边界)
String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk1NDEsImV4cCI6MTU2Njc5OTE0MX0";
// 测试5: 解密Payload
byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
System.out.println(new String(payload));
// 测试6: 这是一个被篡改的token,因此会报异常,说明JWT是安全的
jwtOperator.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk3MzIsImV4cCI6MTU2Njc5OTMzMn0.nDv25ex7XuTlmXgNzGX46LqMZItVFyNHQpmL9UQf-aUx");
}
}
JWT分为三段,头部,以及payload,还有签名,可以使用工具类测试
可以将该密钥写入配置类,这里涉及一个配置技巧
jwt:
secret: 秘钥
# 有效期,单位秒,默认2周
expire-time-in-second: 1209600
登录方法
需要我们写一个 /users/login
的接口
新建一个类 JwtTokenRespDTO
,存储展示的Token信息
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class JwtTokenRespDTO {
private String token;
private Long expirationTime;
}
类 UserRespDTO
,存储展示的用户信息
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class UserRespDTO {
/**
* id
*/
private Integer id;
/**
* 头像地址
*/
private String avatarUrl;
/**
* 积分
*/
private String bonus;
/**
* 微信昵称
*/
private String wxNickname;
}
类 LoginRespDTO
,用来返回登录信息(用户信息以及Token信息)
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class LoginRespDTO {
private JwtTokenRespDTO jwtTokenRespDTO;
private UserRespDTO user;
}
类 UserLoginDTO
,用来微信登录输入消息
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class UserLoginDTO {
private String code;
private String avatarUrl;
private String wxNickname;
}
加入一个依赖
<dependency>
<groupId>com.github.binarywanggroupId>
<artifactId>weixin-java-miniappartifactId>
<version>3.5.0version>
dependency>
业务逻辑
wx登录接口,输入app id以及密钥
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.config.WxMaConfig;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WxConfig {
@Bean
public WxMaConfig wxMaConfig(){
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
// 这边写微信的appId以及
config.setAppid("xxwxf951e829c4a34a45x");
config.setSecret("xxd8bb0f730ffa14782f08e59107c1fc88xx");
return config;
}
@Bean
public WxMaService wxMaService(WxMaConfig wxMaConfig){
WxMaServiceImpl service = new WxMaServiceImpl();
service.setWxMaConfig(wxMaConfig);
return service;
}
}
服务类
public User login(UserLoginDTO loginDTO,String openId){
User user = userMapper.selectOne(new QueryWrapper<User>()
.eq("wx_id", openId));
if (user == null){
User userToSave = User.builder()
.wxId(openId)
.bonus(300)
.wxNickname(loginDTO.getWxNickname())
.avaterUrl(loginDTO.getAvatarUrl())
.roles("user")
.createTime(new Date())
.updateTime(new Date())
.build();
userMapper.insert(userToSave);
return userToSave;
}
return user;
}
写登录api,具体逻辑在注释里已经标有
@PostMapping("/login")
public LoginRespDTO login(@RequestBody UserLoginDTO loginDTO) throws WxErrorException {
// 校验是否登录
WxMaJscode2SessionResult result = wxMaService.getUserService()
.getSessionInfo(loginDTO.getCode());
System.out.println("code:"+loginDTO.getCode());
// 微信的唯一标示
String openid = result.getOpenid();
// 看用户是否在用户中心注册,如果没有就插入数据库
// 之后颁发token
User user = userService.login(loginDTO, openid);
// 颁发jwt,token
HashMap<String, Object> userInfo = Maps.newHashMap();
userInfo.put("id",user.getId());
userInfo.put("wxNickname",user.getWxNickname());
userInfo.put("role",user.getRoles());
String token = jwtOperator.generateToken(userInfo);
// 输出日志
log.info("用户{}登陆成功,生成的token={},有效期到:{}",
loginDTO.getWxNickname(),
token,
jwtOperator.getExpirationTime());
UserRespDTO userRespDTO = UserRespDTO.builder()
.id(user.getId())
.avatarUrl(user.getAvaterUrl())
.bonus(user.getBonus())
.wxNickname(user.getWxNickname())
.build();
JwtTokenRespDTO jwtTokenRespDTO = JwtTokenRespDTO.builder()
.expirationTime(jwtOperator.getExpirationTime().getTime())
.token(token)
.build();
LoginRespDTO loginRespDTO = LoginRespDTO.builder()
.user(userRespDTO)
.jwtTokenRespDTO(jwtTokenRespDTO)
.build();
return loginRespDTO;
}
需要预备知识
注解知识
AOP知识
定义一个注解
思路
因为 /users/id
只能在登录后访问,因此需要状态检查
第三种方式可插拔,AOP可以实现登录检查
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
@CheckLogin
,默认形式即可public @interface CheckLogin {
}
这里使用了Around,其实也可以使用before
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class CheckLoginAspect {
private final JwtOperator jwtOperator;
@Pointcut("@annotation(com.zhou.usercenter.auth.CheckLogin)")
public void checkLoginPointCut(){
}
@Around("checkLoginPointCut()")
public Object checkLogin(ProceedingJoinPoint jp) throws Throwable {
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 = 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"));
return jp.proceed();
} catch (Throwable throwable) {
throw new SecurityException("Token不合法");
}
}
}
3.1 (万能获得Request的方法)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
@RestControllerAdvice
异常统一拦截返回
ResponseEntity的用法:返回体+返回响应码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionErrorHandler {
@ExceptionHandler(SecurityException.class)
public ResponseEntity<ErrorBody> error(SecurityException e){
log.warn("发生SecurityException异常",e);
ErrorBody errorBody = ErrorBody.builder()
.body("Token非法,用户不允许访问")
.status(HttpStatus.UNAUTHORIZED.value())
.build();
ResponseEntity<ErrorBody> response = new ResponseEntity<ErrorBody>(
errorBody, HttpStatus.UNAUTHORIZED
);
return response;
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ErrorBody{
private String body;
private int status;
}
@RestController
@RequestMapping("/users")
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserController {
private final UserServiceImpl userService;
private final WxMaService wxMaService;
private final JwtOperator jwtOperator;
@GetMapping("/{id}")
@CheckLogin
public User findById(@PathVariable("id") Integer id){
log.info("我被请求勒.....");
return userService.findById(id);
}
@GetMapping("/gen-token")
public String genToken(){
// 颁发jwt,token
HashMap<String, Object> userInfo = Maps.newHashMap();
userInfo.put("id",1);
userInfo.put("wxNickname","周杰伦");
userInfo.put("role","user");
return jwtOperator.generateToken(userInfo);
}
@PostMapping("/login")
public LoginRespDTO login(@RequestBody UserLoginDTO loginDTO) throws WxErrorException {
// 校验是否登录
WxMaJscode2SessionResult result = wxMaService.getUserService()
.getSessionInfo(loginDTO.getCode());
System.out.println("code:"+loginDTO.getCode());
// 微信的唯一标示
String openid = result.getOpenid();
// 看用户是否在用户中心注册,如果没有就插入数据库
// 之后颁发token
User user = userService.login(loginDTO, openid);
// 颁发jwt,token
HashMap<String, Object> userInfo = Maps.newHashMap();
userInfo.put("id",user.getId());
userInfo.put("wxNickname",user.getWxNickname());
userInfo.put("role",user.getRoles());
String token = jwtOperator.generateToken(userInfo);
log.info("用户{}登陆成功,生成的token={},有效期到:{}",
loginDTO.getWxNickname(),
token,
jwtOperator.getExpirationTime());
UserRespDTO userRespDTO = UserRespDTO.builder()
.id(user.getId())
.avatarUrl(user.getAvaterUrl())
.bonus(user.getBonus())
.wxNickname(user.getWxNickname())
.build();
JwtTokenRespDTO jwtTokenRespDTO = JwtTokenRespDTO.builder()
.expirationTime(jwtOperator.getExpirationTime().getTime())
.token(token)
.build();
LoginRespDTO loginRespDTO = LoginRespDTO.builder()
.user(userRespDTO)
.jwtTokenRespDTO(jwtTokenRespDTO)
.build();
return loginRespDTO;
}
}
之后访问gen-token
获得登录token,再将其带到header中访问/users/{id}
,即可实现登录授权
使用Feign调用用户中心的时候没有传递Token
因此需要用Feign传递Token
@RequestHeader
方法shares/{id}
api
@GetMapping("/{id}")
@CheckLogin
public ShareDTO findById(
@PathVariable("id") Integer id,
@RequestHeader("X-Token") String token){
ShareDTO shareDTO = shareService.findById(id,token);
return shareDTO;
}
service类
public ShareDTO findById(Integer id,String token)
Feign类
@FeignClient(name="user-center"
// , fallbackFactory = UserCenterFeignClientFallbackFactory.class
)
public interface UserCenterFeignClient {
@GetMapping("/users/{id}")
UserDTO findById(@PathVariable("id") Integer id,
@RequestHeader("X-Token") String token);
}
全局拦截器,使用requestTemplate传递Header
public class TokenRelayRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// 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)){
requestTemplate.header("X-Token",token);
}
}
}
之后在application.yml
中配置全局配置
feign:
client:
config:
default:
loggerLevel: full
requestInterceptors:
- com.zhou.contentcenter.feignclient.interceptor.TokenRelayRequestInterceptor
exchange方法
@Autowired
private RestTemplate restTemplate;
@GetMapping("/tokenRelay/{userId}")
public ResponseEntity<UserDTO> userDTO(@PathVariable("userId") Integer userId,
HttpServletRequest request){
String token = request.getHeader("X-Token");
HttpHeaders headers = new HttpHeaders();
headers.add("X-Token",token);
ResponseEntity<UserDTO> exchange = restTemplate.exchange(
"http://user-center/users/{userId}",
HttpMethod.GET,
new HttpEntity<>(headers), //
UserDTO.class, // 响应体类型
userId // URL参数
);
return exchange;
}
ClientHttpRequestInterceptor
public class RestTemplateTokenRelayInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest httpServletRequest = attributes.getRequest();
String token = httpServletRequest.getHeader("X-Token");
HttpHeaders headers = request.getHeaders();
headers.add("X-Token",token);
// 保证请求继续执行
return execution.execute(request,body);
}
}
configuration类
@Configuration
public class ContentRestTemplateConfig {
@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(
Collections.singletonList(
new RestTemplateTokenRelayInterceptor()
)
);
return restTemplate;
}
}
controller类
@GetMapping("/tokenRelay2/{userId}")
public UserDTO userDTO2(@PathVariable("userId") Integer userId){
UserDTO userDTO = restTemplate.getForObject(
"http://user-center/users/{userId}",
UserDTO.class,userId);
return userDTO;
}
加入Request判断
但是每次更改都很麻烦,如果有很多接口则需要更改好几个接口,有些麻烦
@PutMapping("/audit/{id}")
@CheckLogin
public Share auditById(@PathVariable("id") Integer id,
@RequestBody ShareAuditDTO auditDTO,
HttpServletRequest httpServletRequest){
String role = (String) httpServletRequest.getAttribute("role");
log.info("当前角色是:{}",role);
// TODO 认证授权
return shareService.auditById(id,auditDTO);
}
Spring AOP注解方式实现权限验证
写一个注解,记得加入Retention
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthorization {
String value();
}
切面
获取注解中的名字值得学习
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class CheckAuthorizationAspect {
@Autowired
private JwtOperator jwtOperator;
@Pointcut("@annotation(com.zhou.contentcenter.auth.CheckAuthorization)")
public void CheckAuthorizationPointCut(){
}
@Around("CheckAuthorizationPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
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 = jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("Token不合法");
}
// 3.校验成功,将用户信息设置到request的attribute里
// 从生成Token的Claims里边获取值,输入到Request中
Claims claims = jwtOperator.getClaimsFromToken(token);
request.setAttribute("id", claims.get("id"));
request.setAttribute("wxNickname", claims.get("wxNickname"));
request.setAttribute("role", claims.get("role"));
String role = (String) claims.get("role");
System.out.println("用户的角色是:"+role);
// 获取到Signature的名称
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
CheckAuthorization annotation = method.getAnnotation(CheckAuthorization.class);
String value = annotation.value();
System.out.println("用户的角色是:"+value);
if (!Objects.equals(value, role)) {
throw new SecurityException("用户无权访问");
}
}catch (Throwable throwable){
throw new SecurityException("用户无权访问",throwable);
}
return point.proceed();
}
}
定义好上述之后,只需在需要控制权限的接口controller函数上加上注解@CheckAuthorization("admin")
即可实现对登录用户的权限访问