练习Spring Cloud Alibaba 7 ——用户认证&授权

用户认证&授权

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类实现feignRequestInterceptor接口

/**
 * @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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值