您的位置:首页 > 其它

乐优商城:笔记(十二):鉴权微服务——授权

2019-06-05 22:10 393 查看
版权声明:转载请说明 https://blog.csdn.net/sinat_38570489/article/details/90904796

文章目录

  • 2 JWT工具类
  • 3 编写授权登陆接口
  • 4 登录
  • 5 校验登录状态
  • 1 创建授权中心

    授权中心的主要职责:

    • 用户鉴权: 接收用户的登录请求,通过用户中心的接口进行校验,通过后生成JWT
    • 使用私钥生成JWT并返回
  • 服务鉴权:微服务间的调用不经过Zuul,会有风险,需要鉴权中心进行认证
      原理与用户鉴权类似,但逻辑稍微复杂一些(此处我们不做实现)

    因为生成JWT,解析JWT这样的行为以后在其它微服务中也会用到,因此我们会抽取成工具。我们把鉴权中心进行聚合,一个工具module,一个提供服务的module

    1.1 创建父module

    我们先创建父module,名称为:ly-auth-center,将pom打包方式改为pom

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
    <artifactId>leyou</artifactId>
    <groupId>com.leyou.parent</groupId>
    <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.leyou.service</groupId>
    <artifactId>ly-auth-center</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <modules>
    <module>ly-auth-common</module>
    </modules>
    <packaging>pom</packaging>
    </project>

    1.2 授权服务的通用模块:ly-auth-common

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
    <artifactId>ly-auth-center</artifactId>
    <groupId>com.leyou.service</groupId>
    <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.leyou.service</groupId>
    <artifactId>ly-auth-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    </project>

    1.3 授权服务的业务模块:ly-auth-service

    1.3.1 引入依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
    <artifactId>ly-auth-center</artifactId>
    <groupId>com.leyou.service</groupId>
    <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.leyou.service</groupId>
    <artifactId>ly-auth-service</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>com.leyou.service</groupId>
    <artifactId>ly-auth-common</artifactId>
    <version>${leyou.latest.version}</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    </dependencies>
    </project>

    1.3.2 配置文件

    server:
    port: 8087
    spring:
    application:
    name: auth-service
    eureka:
    client:
    service-url:
    defaultZone: http://127.0.0.1:10086/eureka
    registry-fetch-interval-seconds: 10
    instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1

    1.3.3 启动类

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    public class LyAuthApplication {
    public static void main(String[] args) {
    SpringApplication.run(LyAuthApplication.class, args);
    }
    }

    1.3.4 修改路由

    zuul:
    prefix: /api # 添加路由前缀
    retryable: true
    routes:
    item-service: /item/**
    search-service: /search/**
    user-service: /user/**
    auth-service: /auth/**

    最终结构:

    2 JWT工具类

    我们在

    ly-auth-coomon
    中编写一些通用的工具类:

    2.1 RSA工具类:RsaUtils

    public class RsaUtils {
    /**
    * 从文件中读取公钥
    *
    * @param filename 公钥保存路径,相对于classpath
    * @return 公钥对象
    * @throws Exception
    */
    public static PublicKey getPublicKey(String filename) throws Exception {
    byte[] bytes = readFile(filename);
    return getPublicKey(bytes);
    }
    
    /**
    * 从文件中读取密钥
    *
    * @param filename 私钥保存路径,相对于classpath
    * @return 私钥对象
    * @throws Exception
    */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
    byte[] bytes = readFile(filename);
    return getPrivateKey(bytes);
    }
    
    /**
    * 获取公钥
    *
    * @param bytes 公钥的字节形式
    * @return
    * @throws Exception
    */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
    X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
    KeyFactory factory = KeyFactory.getInstance("RSA");
    return factory.generatePublic(spec);
    }
    
    /**
    * 获取密钥
    *
    * @param bytes 私钥的字节形式
    * @return
    * @throws Exception
    */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
    KeyFactory factory = KeyFactory.getInstance("RSA");
    return factory.generatePrivate(spec);
    }
    
    /**
    * 根据密文,生成rsa公钥和私钥,并写入指定文件
    *
    * @param publicKeyFilename  公钥文件路径
    * @param privateKeyFilename 私钥文件路径
    * @param secret             生成密钥的密文
    * @throws IOException
    * @throws NoSuchAlgorithmException
    */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    SecureRandom secureRandom = new SecureRandom(secret.getBytes());
    keyPairGenerator.initialize(1024, secureRandom);
    KeyPair keyPair = keyPairGenerator.genKeyPair();
    // 获取公钥并写出
    byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
    writeFile(publicKeyFilename, publicKeyBytes);
    // 获取私钥并写出
    byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
    writeFile(privateKeyFilename, privateKeyBytes);
    }
    
    private static byte[] readFile(String fileName) throws Exception {
    return Files.readAllBytes(new File(fileName).toPath());
    }
    
    private static void writeFile(String destPath, byte[] bytes) throws IOException {
    File dest = new File(destPath);
    if (!dest.exists()) {
    dest.createNewFile();
    }
    Files.write(dest.toPath(), bytes);
    }
    }

    2.2 常量类:JwtConstans

    定义了jwt中的payload的常用key

    public abstract class JwtConstans {
    public static final String JWT_KEY_ID = "id";
    public static final String JWT_KEY_USER_NAME = "username";
    }

    2.3 载荷:UserInfo

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class UserInfo {
    private Long id;
    private String username;
    }

    2.4 对象工具类:ObjectUtils

    从jwt解析得到的数据是Object类型,转换为具体类型可能出现空指针,这个工具类进行了一些转换:

    public class ObjectUtils {
    
    public static String toString(Object obj) {
    if (obj == null) {
    return null;
    }
    return obj.toString();
    }
    
    public static Long toLong(Object obj) {
    if (obj == null) {
    return 0L;
    }
    if (obj instanceof Double || obj instanceof Float) {
    return Long.valueOf(StringUtils.substringBefore(obj.toString(), "."));
    }
    if (obj instanceof Number) {
    return Long.valueOf(obj.toString());
    }
    if (obj instanceof String) {
    return Long.valueOf(obj.toString());
    } else {
    return 0L;
    }
    }
    
    public static Integer toInt(Object obj) {
    return toLong(obj).intValue();
    }
    }

    2.5 JWT工具类

    我们已经在

    ly-auth-common
    中引入JWT依赖:jjwt和开源的时间/日期库:joda-time

    public class JwtUtils {
    /**
    * 私钥加密token
    *
    * @param userInfo      载荷中的数据
    * @param privateKey    私钥
    * @param expireMinutes 过期时间,单位秒
    * @return
    * @throws Exception
    */
    public static String generateToken(UserInfo userInfo, PrivateKey privateKey, int expireMinutes) throws Exception {
    return Jwts.builder()
    .claim(JwtConstans.JWT_KEY_ID, userInfo.getId())
    .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername())
    .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
    .signWith(SignatureAlgorithm.RS256, privateKey)
    .compact();
    }
    
    /**
    * 私钥加密token
    *
    * @param userInfo      载荷中的数据
    * @param privateKey    私钥字节数组
    * @param expireMinutes 过期时间,单位秒
    * @return
    * @throws Exception
    */
    public static String generateToken(UserInfo userInfo, byte[] privateKey, int expireMinutes) throws Exception {
    return Jwts.builder()
    .claim(JwtConstans.JWT_KEY_ID, userInfo.getId())
    .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername())
    .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
    .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey))
    .compact();
    }
    
    /**
    * 公钥解析token
    *
    * @param token     用户请求中的token
    * @param publicKey 公钥
    * @return
    * @throws Exception
    */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
    return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }
    
    /**
    * 公钥解析token
    *
    * @param token     用户请求中的token
    * @param publicKey 公钥字节数组
    * @return
    * @throws Exception
    */
    private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception {
    return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey))
    .parseClaimsJws(token);
    }
    
    /**
    * 获取token中的用户信息
    *
    * @param token     用户请求中的令牌
    * @param publicKey 公钥
    * @return 用户信息
    * @throws Exception
    */
    public static UserInfo getInfoFromToken(String token, PublicKey publicKey) throws Exception {
    Jws<Claims> claimsJws = parserToken(token, publicKey);
    Claims body = claimsJws.getBody();
    return new UserInfo(
    ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),
    ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME))
    );
    }
    
    /**
    * 获取token中的用户信息
    *
    * @param token     用户请求中的令牌
    * @param publicKey 公钥
    * @return 用户信息
    * @throws Exception
    */
    public static UserInfo getInfoFromToken(String token, byte[] publicKey) throws Exception {
    Jws<Claims> claimsJws = parserToken(token, publicKey);
    Claims body = claimsJws.getBody();
    return new UserInfo(
    ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),
    ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME))
    );
    }
    }

    3 编写授权登陆接口

    接下来,我们需要在

    ly-auth-servcice
    编写一个接口,对外提供登录授权服务。基本流程如下:

    • 客户端携带用户名和密码请求登录
    • 授权中心调用客户中心接口,根据用户名和密码查询用户信息
    • 如果用户名密码正确,能获取用户,否则登录失败
    • 如果校验成功,则生成JWT并返回

    3.1 生成公钥和私钥

    公钥和私钥要在项目一启动时就生成

    我们需要在授权中心生成真正的公钥和私钥。我们必须有一个生成公钥和私钥的secret,这个可以配置到

    application.yml
    中:

    ly:
    jwt:
    secret: ly@Login(Auth}*^31)&yun6%f3q2 # 登录校验的密钥
    pubKeyPath: H:/javacode/idea/rsa/rsa.pub # 公钥地址
    priKeyPath: H:/javacode/idea/rsa/rsa.pri # 私钥地址
    expire: 30 # 过期时间,单位分钟
    cookieName: LY_TOKEN

    然后编写属性类,加载这些数据:

    @Data
    @ConfigurationProperties(prefix = "ly.jwt")
    public class JwtProperties {
    
    private String secret; // 密钥
    private String pubKeyPath;// 公钥
    private String priKeyPath;// 私钥
    private int expire;// token过期时间
    private String cookieName;
    
    private PublicKey publicKey; // 公钥
    private PrivateKey privateKey; // 私钥
    
    private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
    
    // 对象一旦实例化后,就应该读取公钥和私钥
    @PostConstruct // 构造函数执行完毕后就执行
    public void init(){
    try {
    //公钥和私钥不存在  要先生成
    File pubKey = new File(pubKeyPath);
    File priKey = new File(priKeyPath);
    if (!pubKey.exists() || !priKey.exists()) {
    // 生成公钥和私钥
    RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);
    }
    // 获取公钥和私钥
    this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
    this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
    } catch (Exception e) {
    logger.error("初始化公钥和私钥失败!", e);
    throw new RuntimeException();
    }
    }
    }
    • @ConfigurationProperties

      可以把读取配置文件的信息,并自动封装成实体类
    • @PostConstruct
      : 被
      @PostConstruct
      修饰的方法会在服务器加载
      Servlet
      的时候运行(对象一旦实例化后,就执行),并且只会被服务器调用一次,类似于
      Serclet
      inti()
      方法。被
      @PostConstruc
      t修饰的方法会在构造函数之后,init()方法之前运行。
    • 如果想在生成对象时完成某些初始化操作,而偏偏这些初始化操作又依赖于依赖注入,那么就无法在构造函数中实现。为此,可以使用
      @PostConstruct
      注解一个方法来完成初始化,
      @PostConstruct
      注解的方法将会在依赖注入完成后被自动调用。
      顺序:Constructor >> @Autowired >> @PostConstruct

    参考资料:

    1. Java开发之@PostConstruct和@PreDestroy注解
    2. @PostConstruct详解

    注:理论上公钥私钥只生成一次,不会无限生成

    3.2 controller


    编写授权接口,我们接收用户名和密码,校验成功后,写入cookie中,那如何写入cookie中呢?通过

    HttpServletResponse response
    ,
    HttpServletRequest request
    ,以及
    ly-common
    中的
    CookieUtils
    工具类(该工具类利用的工厂模式来创建)

    • 将token写入cookie — 工厂模式 httpOnly():避免别的js代码来操作你的cookie,是一种安全措施
    • charset():不需要编码 因为token中没有中文
    • maxAge(): cookie的生命周期,默认是-1,代表一次会话,浏览器关闭cookie就失效

    为什么要用到response 和 request 呢?

    • response:将cookie写入 — response中有一个方法 addCookie()
    • request:cookie中有的概念 domain 例如一个cookie只能在www.baidu.com生效,无法在别的域下生效, 给cookie绑定一个域,防止别的网站访问你的cookie,也是一种安全措施
    @Value("${ly.jwt.cookieName}")
    private String cookieName;
    
    @PostMapping("login")
    public ResponseEntity<Void> login(// 无需给前端浏览器返回token,token只有后端才需要使用,并且将token保存到cookie中
    @RequestParam("username") String username,
    @RequestParam("password") String password,
    HttpServletResponse response,
    HttpServletRequest request){
    // 登录功能的实现
    String token = authService.login(username, password);
    
    // 将token写入cookie --- 工厂模式
    CookieUtils.newBuilder(response).httpOnly().request(request).build(cookieName, token);
    
    return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
    }
    • @value
      :Spring 通过注解获取*.porperties 或 *.yml文件的内容,除了xml配置外,还可以通过@value方式来获取,这种注解方式比较麻烦,需要自己写全路径,
      @ConfigurationProperties
      方式比较简单,直接全部注入进来

    3.3 UserClient

    接下来我们肯定要对用户密码进行校验,所以我们需要通过FeignClient去访问

    user-service
    微服务:

    引入user-service依赖:

    <dependency>
    <groupId>com.leyou.service</groupId>
    <artifactId>ly-user-interface</artifactId>
    <version>${leyou.latest.version}</version>
    </dependency>

    编写FeignClient:

    @FeignClient(value = "user-service")
    public interface UserClient extends UserApi {
    }

    3.4 service

    public String login(String username, String password) {
    
    try {
    // 校验用户名和密码
    User user = userClient.queryUsernameAndPassword(username, password);
    if (user == null) {
    throw new LyException(ExceptionEnum.INVALID_USERNAME_PASSWORD);
    }
    // 生成token
    String token = JwtUtils.generateToken(new UserInfo(user.getId(), username), prop.getPrivateKey(), prop.getExpire());
    return token;
    }catch (Exception e){
    log.error("[授权中心] 用户名或者密码有误,用户名称:{}", username, e);
    throw new LyException(ExceptionEnum.INVALID_USERNAME_PASSWORD);
    }
    }

    4 登录

    接下来,我们看看登录页面,是否能够正确的发出请求。

    我们在页面输入登录信息,然后点击登录:

    成功跳转到了首页:

    然后去看cookie:

    什么都没有,为什么?——我们在下一篇博客中分析:解决cookie无法写入的问题

    5 校验登录状态

    当cookie无法写入的问题得到解决以后,访问首页,发现首页的顶部,登录状态依然没能判断出用户信息:


    这里需要向后台发起请求,获取根据cookie获取当前用户的信息。

    因为token在cookie中,因此本次请求肯定会携带token信息在头中。

    5.1 controller

    我们在

    ly-auth-service
    中定义用户的校验接口,通过cookie获取token,然后校验通过返回用户信息。

    • 请求方式:GET
    • 请求路径:/verify
    • 请求参数:无,不过我们需要从cookie中获取token信息
    • 返回结果:UserInfo,校验成功返回用户信息;校验失败,则返回401

    代码:

    @GetMapping("verify")
    public ResponseEntity<UserInfo> verify(
    @CookieValue("LY_TOKEN") String token,
    HttpServletResponse response,
    HttpServletRequest request
    ){
    
    try {
    // 解析token
    UserInfo info = JwtUtils.getInfoFromToken(token, prop.getPublicKey());
    
    // 刷新token并重新写入
    String newToken = JwtUtils.generateToken(info, prop.getPrivateKey(), prop.getExpire());
    CookieUtils.newBuilder(response).httpOnly().request(request).build(cookieName, newToken);
    
    return ResponseEntity.ok(info);
    } catch (Exception e){
    throw new LyException(ExceptionEnum.NO_AUTHORIZED);
    }
    }

    获取cookie并不非得从request中取出,可以使用注解

    @CookieValue

    • 作用:用来获取Cookie中的值
    • 参数: value:参数名称
    • required:是否必须
    • defaultValue:默认值

    注:只要用户有操作就应该重新刷新token(重新生成token并写入),否则用户在浏览商品的时候不经意间过了30分钟有效期,准备下单时让用户重新登陆,影响用户体验。

    到此为止,我们仅仅完成了对用户的授权,接下来,我们完成鉴权,见下一篇博客鉴权微服务——鉴权

  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: