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

文章目录

  • 1 创建授权中心
    • 1.1 创建父module
    • 1.2 授权服务的通用模块:ly-auth-common
    • 1.3 授权服务的业务模块:ly-auth-service
      • 1.3.1 引入依赖
      • 1.3.2 配置文件
      • 1.3.3 启动类
      • 1.3.4 修改路由
  • 2 JWT工具类
    • 2.1 RSA工具类:RsaUtils
    • 2.2 常量类:JwtConstans
    • 2.3 载荷:UserInfo
    • 2.4 对象工具类:ObjectUtils
    • 2.5 JWT工具类
  • 3 编写授权登陆接口
    • 3.1 生成公钥和私钥
    • 3.2 controller
    • 3.3 UserClient
    • 3.4 service
  • 4 登录
  • 5 校验登录状态
    • 5.1 controller

1 创建授权中心

授权中心的主要职责:

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

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

1.1 创建父module

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


<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>leyouartifactId>
        <groupId>com.leyou.parentgroupId>
        <version>1.0.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <groupId>com.leyou.servicegroupId>
    <artifactId>ly-auth-centerartifactId>
    <version>1.0.0-SNAPSHOTversion>
    <modules>
        <module>ly-auth-commonmodule>
    modules>
    <packaging>pompackaging>
project>

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


<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-centerartifactId>
        <groupId>com.leyou.servicegroupId>
        <version>1.0.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <groupId>com.leyou.servicegroupId>
    <artifactId>ly-auth-commonartifactId>
    <version>1.0.0-SNAPSHOTversion>
project>

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

1.3.1 引入依赖


<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-centerartifactId>
        <groupId>com.leyou.servicegroupId>
        <version>1.0.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <groupId>com.leyou.servicegroupId>
    <artifactId>ly-auth-serviceartifactId>
    <version>1.0.0-SNAPSHOTversion>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>com.leyou.servicegroupId>
            <artifactId>ly-auth-commonartifactId>
            <version>${leyou.latest.version}version>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        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/**

最终结构:
乐优商城:笔记(十二):鉴权微服务——授权_第1张图片

2 JWT工具类

我们在ly-auth-coomon中编写一些通用的工具类:
乐优商城:笔记(十二):鉴权微服务——授权_第2张图片

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的时候运行(对象一旦实例化后,就执行),并且只会被服务器调用一次,类似于Sercletinti()方法。被@PostConstruct修饰的方法会在构造函数之后,init()方法之前运行。
    • 如果想在生成对象时完成某些初始化操作,而偏偏这些初始化操作又依赖于依赖注入,那么就无法在构造函数中实现。为此,可以使用@PostConstruct注解一个方法来完成初始化,@PostConstruct注解的方法将会在依赖注入完成后被自动调用
      顺序:Constructor >> @Autowired >> @PostConstruct

参考资料:

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

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

3.2 controller

乐优商城:笔记(十二):鉴权微服务——授权_第3张图片
编写授权接口,我们接收用户名和密码,校验成功后,写入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.servicegroupId>
    <artifactId>ly-user-interfaceartifactId>
    <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 登录

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

我们在页面输入登录信息,然后点击登录:
乐优商城:笔记(十二):鉴权微服务——授权_第4张图片

成功跳转到了首页:
乐优商城:笔记(十二):鉴权微服务——授权_第5张图片
然后去看cookie:
乐优商城:笔记(十二):鉴权微服务——授权_第6张图片
什么都没有,为什么?——我们在下一篇博客中分析:解决cookie无法写入的问题


5 校验登录状态

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

乐优商城:笔记(十二):鉴权微服务——授权_第7张图片
这里需要向后台发起请求,获取根据cookie获取当前用户的信息。
乐优商城:笔记(十二):鉴权微服务——授权_第8张图片
因为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分钟有效期,准备下单时让用户重新登陆,影响用户体验。


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

你可能感兴趣的:(乐优商城)