• 从零搭建基于SpringCloud Alibaba 鉴权中心服务(详细教程)


    鉴权中心服务

    认识JWT

    json web token 是一个开放的标准 ,它定义了一个种紧凑的,自包含的方式,用于作为json对象在各方之间安全的传输信息

    • 服务器鉴权完成之后 会生成 json 对象 发送给客户端,之后客户端和服务端传输数据都需要带上这个对象,服务器完全通过这个json对象认定客户端身份,为了防止篡改数据,服务端在生成的时候都会加上签名(加密的意思),服务器不保存session数据也就是无状态,更适合实现扩展

    • 那些环境可以考虑使用jwt呢?用户授权 ,信息交换

    JWT组成部分

    • Header :头部信息

    Header 由两部分组成(Token类型,加密算法的名称),并且使用的是base64的编码

    • Payload:我们想要传递的数据

    Payload KV形式的诗数据 ,这里就是我们想要传递的信息(授权的话就是Token信息)

    • Signature :签名

    Signature 为了得到签名 首先我们得有编码过的Header 编码过的payload 和一个密钥。签名用的算法就是header中指定的那个,之后就会对他们签名

    我们需要一个签名公式

    HMACSHA245(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
    

    产生一个签名,返回一个字符串,返回给客户端,之后客户端每次访问都要带上这个字符串,进行鉴权

    JWT使用.号来连接 HHH.PPPP.SSSS

    授权,鉴权设计

    这里我们先不考虑 gateway 网关,后续会搭建,我们的重点放在中间和右边部分

    鉴权部分,我们独立实现公共的工具类,为什么?以下三点

     

    • JWT本质上是通过算法算出的加密字符串,也可以通过算法反向解析出来,他不依赖任何的框架,所以这个功能有可以单独提取出来的前提

    • 我们的电商系统包含多个微服务,很显然我们每个服务都需要鉴权,于是我们把这个方法提取出来,方便复用

    • 高性能鉴权,为什么不在授权中心做鉴权,首先他回头过http请求等一系列操作,我们在本地只用java的话 少去了很多步骤,性能得到倍数的增长

    授权编码实现

    我们创建新的一个服务来编写我们的鉴权中心

    e-commerce-authority-center

    导入相关的依赖

    1. <dependencies>
    2.     
    3.     <dependency>
    4.         <groupId>com.alibaba.cloudgroupId>
    5.         <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    6.         <version>2.2.3.RELEASEversion>
    7.     dependency>
    8.     
    9.     <dependency>
    10.         <groupId>org.springframework.bootgroupId>
    11.         <artifactId>spring-boot-starter-data-jpaartifactId>
    12.     dependency>
    13.     
    14.     <dependency>
    15.         <groupId>mysqlgroupId>
    16.         <artifactId>mysql-connector-javaartifactId>
    17.         <version>8.0.12version>
    18.         <scope>runtimescope>
    19.     dependency>
    20.     <dependency>
    21.         <groupId>com.hyc.ecommercegroupId>
    22.         <artifactId>e-commerce-mvc-configartifactId>
    23.         <version>1.0-SNAPSHOTversion>
    24.     dependency>
    25.     
    26.     <dependency>
    27.         <groupId>org.springframework.cloudgroupId>
    28.         <artifactId>spring-cloud-starter-zipkinartifactId>
    29.     dependency>
    30.     <dependency>
    31.         <groupId>org.springframework.kafkagroupId>
    32.         <artifactId>spring-kafkaartifactId>
    33.         <version>2.5.0.RELEASEversion>
    34.     dependency>
    35.     
    36.     <dependency>
    37.         <groupId>org.freemarkergroupId>
    38.         <artifactId>freemarkerartifactId>
    39.         <version>2.3.30version>
    40.     dependency>
    41.     <dependency>
    42.         <groupId>cn.smallbun.screwgroupId>
    43.         <artifactId>screw-coreartifactId>
    44.         <version>1.0.3version>
    45.     dependency>
    46. dependencies>

    导入好依赖之后我们 编写对应的配置,如注册到naocs 加入adminserver的监管,配置数据源等 这里我们使用jpa 来做orm

    • 配置编写

    1. server:
    2.   port: 7000
    3.   servlet:
    4.     context-path: /ecommerce-authority-center
    5. spring:
    6.   application:
    7.     name: e-commerce-authority-center
    8.   cloud:
    9.     nacos:
    10.       discovery:
    11.         enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
    12.         server-addr: 127.0.0.1:8848 # Nacos 服务器地址
    13.         # server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
    14.         namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
    15.         metadata:
    16.           management:
    17.             context-path: ${server.servlet.context-path}/actuator
    18.   jpa:
    19.     show-sql: true
    20.     hibernate:
    21.       ddl-auto: none
    22.     properties:
    23.       hibernate.show_sql: true
    24.       hibernate.format_sql: true
    25.     open-in-view: false
    26.   datasource:
    27.     # 数据源
    28.     url: jdbc:mysql://127.0.0.1:3306/imooc_e_commerce?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
    29.     username: root
    30.     password: root
    31.     type: com.zaxxer.hikari.HikariDataSource
    32.     driver-class-name: com.mysql.cj.jdbc.Driver
    33.     # 连接池
    34.     hikari:
    35.       maximum-pool-size: 8
    36.       minimum-idle: 4
    37.       idle-timeout: 30000
    38.       connection-timeout: 30000
    39.       max-lifetime: 45000
    40.       auto-commit: true
    41.       pool-name: ImoocEcommerceHikariCP
    42.   kafka:
    43.     bootstrap-servers: 127.0.0.1:9092
    44.     producer:
    45.       retries: 3
    46.     consumer:
    47.       auto-offset-reset: latest
    48.   zipkin:
    49.     sender:
    50.       type: kafka # 默认是 web
    51.     base-url: http://127.0.0.1:9411/
    52. # 暴露端点
    53. management:
    54.   endpoints:
    55.     web:
    56.       exposure:
    57.         include: '*'
    58.   endpoint:
    59.     health:
    60.       show-details: always

    配置完成之后,编写主启动类 @EnableJpaAuditing因为我们用到 自动加入创建时间和修改时间,所以我们需要打开 jpa的自动审计功能,不然会报错

    1. @EnableJpaAuditing //允许 jpa 的自动审计
    2. @SpringBootApplication
    3. @EnableDiscoveryClient
    4. public class AuthorityApplication {
    5.     public static void main(String[] args) {
    6.         SpringApplication.run(AuthorityApplication.class, args);
    7.     }
    8. }

    test包下就测试环境是否正确

    1. /**
    2.  * 授权中心测试入口
    3.  * 验证授权中心 环境可用性
    4.  */
    5. @SpringBootTest
    6. @RunWith(SpringRunner.class)
    7. public class AuthorityCenterApplicationTest {
    8.     @Test
    9.     public void conetextLoad() {
    10.     }
    11. }

    环境ok之后

    我们去测试 数据库操作是否可用

    编写实体类ecommerceUser

    1. /*
    2.  * 用户表实体类定义
    3.  * */
    4. @Entity
    5. @EntityListeners(AuditingEntityListener.class)
    6. @Table(name = "t_ecommerce_user")
    7. @Data
    8. @NoArgsConstructor
    9. @AllArgsConstructor
    10. public class EcommerceUser {
    11.     /* 自增组件*/
    12.     @Id
    13.     @GeneratedValue(strategy = GenerationType.IDENTITY)
    14.     @Column(name = "id", nullable = false)
    15.     private long id;
    16.     /*用户名*/
    17.     @Column(name = "username", nullable = false)
    18.     private String username;
    19.     /* MD5 密码*/
    20.     @Column(name = "password", nullable = false)
    21.     private String password;
    22.     /*额外的信息 json 字符串存储*/
    23.     @Column(name = "extra_info", nullable = false)
    24.     private String extraInfo;
    25.     /*自动加入创建时间 需要主启动类的注解*/
    26.     @CreatedDate
    27.     @Column(name = "create_time", nullable = false)
    28.     private Date createTime;
    29.     /*自动加入更新时间 需要主启动类的注解*/
    30.     @CreatedDate
    31.     @Column(name = "update_time", nullable = false)
    32.     private Date updateTime;
    33. }

    有了实体类我们需要有数据操作的实现 于是编写Dao 接口

    其实当我们创建接口的时候jpa就已经有了对应的基础增删改查的方法

    这里我们实现两个自定义查询方法

     

    1. /**
    2.  * EcommerceUserDao 接口定义
    3.  */
    4. public interface EcommerceUserDao extends JpaRepository {
    5.     /*
    6.      * 根据用户名查询 EcommerceUser 对象
    7.      * 等于 select * form t_ecommerce_user where username=?
    8.      * */
    9.     EcommerceUser findByUsername(String name);
    10.     /*
    11.      * 根据用户名查询 EcommerceUser 对象
    12.      * 等于 select * form t_ecommerce_user where username=? and password=?
    13.      * */
    14.     EcommerceUser findByUsernameAndPassword(String name, String password);
    15. }

    之后创建 test service

    1. /**
    2.  
    3.  * @context: EcommerceUser 相关测试
    4.  * @params :  null
    5.  * @return :  * @return : null
    6.  */
    7. @SpringBootTest
    8. @RunWith(SpringRunner.class)
    9. @Slf4j
    10. public class EcommerUserTest {
    11.     @Autowired
    12.     EcommerceUserDao ecommerceUserDao;
    13.     /*测试  新增一个用户数据 */
    14.     @Test
    15.     public void createUserRecord() {
    16.         EcommerceUser ecommerceUser = new EcommerceUser();
    17.         //设置要插入的信息
    18.         ecommerceUser.setUsername("hyc@qq.com");
    19.         ecommerceUser.setPassword(MD5.create().digestHex("123456"));
    20.         ecommerceUser.setExtraInfo("{}");
    21.         //日志打印返回结果
    22.         log.info("server user:[{}]", JSON.toJSON(ecommerceUserDao.save(ecommerceUser)));
    23.     }
    24.     /*测试 我们编写的自定义方法 查询 刚才创建的新角色*/
    25.     @Test
    26.     public void SelectUserInfo() {
    27.         String username = "hyc@qq.com";
    28.         log.info("select userinof:[{}]", JSON.toJSON(ecommerceUserDao.findByUsername(username)));
    29.     }
    30. }

    测试相关的 方法 新增用户啊 或者是 按条件查询用户 ,测试均通过

    生成RSA256的公钥 和 私钥 非对称加密算法

     

    他通过 私钥加密 公钥解密来完成验证,目前很多的鉴权 都是 JWTRSA256的算法来加密鉴权的,如果了解不多,就是用RSA256就可以了

    • 编码

    编写生成公钥密钥的测试类,创建 一些我们常用的VO对象 用来储存我们常用的一些变量,比如用户信息,公钥,密钥,一些常用的属性 放进 VO的模型里

    1. @Slf4j
    2. @SpringBootTest
    3. @RunWith(SpringRunner.class)
    4. /**
    5.  *
    6.  
    7.  * @context: RSA 非对称 加密算法
    8.  * @params :  null 
    9.  * @return :  * @return : null
    10.  */
    11. public class RSATest {
    12.     @Test
    13.     public void generateKeyBytes() throws Exception {
    14.         /*获取到 RSA算法实例*/
    15.         KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    16.         /* 这里最小是 2048 低于的话 是会报错的*/
    17.         keyPairGenerator.initialize(2048);
    18.         /*
    19.          * 生成公钥对
    20.          * */
    21.         KeyPair keyPair = keyPairGenerator.generateKeyPair();
    22.         /*获取 公钥和私钥对象*/
    23.         RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    24.         RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    25.         log.info("private key:[{}]", Base64.encode(privateKey.getEncoded()));
    26.         log.info("public key:[{}]", Base64.encode(publicKey.getEncoded()));
    27.     }
    28. }
    • 创建VO对象保存 我们常用且不会变化的值和对象

    存储私钥 应为是私钥 所以只对鉴权中心 暴露 于是我们在鉴权服务中创建Constant包创建这个AuthotityConstant类保存信息

    1. /**
    2.  
    3.  * @context: 鉴权的常量
    4.  * @params :  null
    5.  * @return :  * @return : null
    6.  */
    7. public class AuthorCanstant {
    8.     /*私钥 只暴露给 鉴权中心 不暴露给任何的其他服务*/
    9.     public static final String PRIVATE_KEY = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBA" +
    10.             "QCMXrQCudalKHJlH16YHr9mI5/xyYnkp5u2gAbMFf2xAHAyykYmixJP3CqG2a8tUwiJjjTIJXP+79Jzgjgg" +
    11.             "VbBaTakrvjeFXz9HNP1D4XD6Li+sRVjnN1iBUwIFRxiFN2EOJflA9bqeQLAge/LgAu06y3jdLLleJF7yDRuMH" +
    12.             "YedqPl9AJa5RdJmt0OgCoVOqacB7oGkFCFISm0Cwjfgq06nyiiULGZNVt8uhDxZAE4Pi2lmf3yggXCBH9AtU/2" +
    13.             "XdyxU9caQJOAbYGxd/mART/NivBjSqo60wcBnktI+booUbDKRBbWRxvfYqKWEwPOwxlJUB3l3pcLZm866Xl3qtVM" +
    14.             "XAgMBAAECggEADCGjLRkik+OK/3JWmo8Nu6YYjKz+XeSecIdgDwNXiZSgHcOdjHc4fe5pPn5RxXkHo9vGdAXIoJ/Z" +
    15.             "cGIwt5qwQx2zITSvV7eDoIPT36n8OaMEO79Cj7kYzRR/eDVMyTagDLj7ccHK/yJYFnaf5vxZxFsRdwwGeTxreD" +
    16.             "/pwZJLxjRSz1W57v5yUJNPPimNB229EogNYHIhQ8+Z7OGiilbtBIL9r6lqlz2hUAVBzXl4kOXFVI+vEodLuV2" +
    17.             "rtQXXrpO1+AgH5lZJ7ahShKbqHt/Q6uJSTKAhbsfv/iadcPjmYp2F7nnYBLf66Jln6AWUwnXrJ7XETOf/+Qcib" +
    18.             "q/5m6RjAQKBgQDruxn+kaDr5uYQMVSHog+CBRBJghJ4JklhY7ZDYJ2wN2KNHOd3mW/wUVDihVIyRFniIzsWU" +
    19.             "0lnI+4OLqNLAZOBaQB5VrjyH4fxn5b26t0xLO1d5EWcOYI8ZRhwWDWaZipe2dUMeqVVMYFeDdTdNsyGrf8x" +
    20.             "L+OVyRDiH4s4pBIs7QKBgQCYcIVFgDbrmwsP7lA9/dU9kClutY3gjEUgB2IJp2Y8S4Xhfi4NC8GqRQoMUyuqg" +
    21.             "vPHKEiTCa1EojGHS/+r4JVcSg9Wsv64SpGZ+gANxRhfYFPrbkjU4YOMaZeCGUfKR2QnD20c3I4gdQ9kU5nK52n+Y" +
    22.             "JEkAFUejg1Mhb6Fp6HDkwKBgAHYYBa3CxxtnUVpLXE2Woq5AWyh4QUhv5dMkYOrgPB9Ln9OR52PDOpDqK9tP" +
    23.             "bx4/n8fqXm+QyfUhyuDP/H5XC86JC/O9vmmN4kzp5ndMsgMwvrmK4lShet1GyDd/+VqgVBmwh0r5JlrHske" +
    24.             "sJjesfEn8YRwDIcCoOg0OQHDfwTtAoGAQfE61YvXNihFqsiOkaKCYjVAlxGWpDJJnMdU05REl4ScD6WDy" +
    25.             "kTxq/RdmmNIGmS3i8mTS3f+Khh3kG2B1ho6wkePRxP7OEGZpqAM8ef22RtUch2tB9neDBmJXtAMzCYB3xu/O" +
    26.             "aL3IHdDB0Va2/krUsz3PDmgmK0ed6HLfwm64l0CgYB+iGkMAQEwqYmcCEXKK825Q9y/u8PE9y8uaMGfsZQzDo6v" +
    27.             "V5v+reOhmZRrk5BnX+pgztbE28sS6c2vYR0RYoR90aD2GXungCPXWEMDQudHFxvSsNTCYkDynjTSlnzu9aDcfqw1" +
    28.             "UIzHog2zCquSro7tnbOMsvV5UdsLBq+WNQGgAw==";
    29.     /*默认的 token 超时时间,一天*/
    30.     public static final Integer DEFAULT_EXPIRE_DAY = 1;
    31. }

    之后是创建一些公共常用的VO模型 e-commerce-common

    • 保存 公钥到公用包 以后我们的服务 需要做授权都需要使用到

    1. /**
    2.  
    3.  * @context: 通用模块的常量定义
    4.  * @params :  null
    5.  * @return :  * @return : null
    6.  */
    7. public class CommonCanstant {
    8.     /* RSA 公钥*/
    9.     public static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjF60ArnWpShyZ" +
    10.             "R9emB6/ZiOf8cmJ5KebtoAGzBX9sQBwMspGJosST9wqhtmvLVMIiY40yCVz/u/Sc4I4IFWwWk2pK743hV8/RzT9Q+F" +
    11.             "w+i4vrEVY5zdYgVMCBUcYhTdhDiX5QPW6nkCwIHvy4ALtOst43Sy5XiRe8g0bjB2Hnaj5fQCWuUXSZrdDoAqFTqmnA" +
    12.             "e6BpBQhSEptAsI34KtOp8oolCxmTVbfLoQ8WQBOD4tpZn98oIFwgR/QLVP9l3csVPXGkCTgG2BsXf5gEU/zYrwY0qqO" +
    13.             "tMHAZ5LSPm6KFGwykQW1kcb32KilhMDzsMZSVAd5d6XC2ZvOul5d6rVTFwIDAQAB";
    14.     /* JWT 中 存储用户信息到 key*/
    15.     public static final String JWT_USER_INFO_KEY = "e-commerce-user";
    16.     /*授权中心的 service-id*/
    17.     public static final String AUTHORITY_CENTER_SERVICE_ID = "e-commerce-authity-center";
    18. }
    • 用户信息的常用VO对象

    JwtToken

    1. /**
    2.  
    3.  * @context: 授权中心 鉴权 之后给客户端的token
    4.  * @params :  null
    5.  * @return :  * @return : null
    6.  */
    7. @Data
    8. @NoArgsConstructor
    9. @AllArgsConstructor
    10. public class JwtToken {
    11.     /* JWT*/
    12.     private String token;
    13. }

    LoginUserinfo

    1. @Data
    2. @NoArgsConstructor
    3. @AllArgsConstructor
    4. public class LoginUserinfo {
    5.     /*用户 id*/
    6.     private Long id;
    7.     /*用户名*/
    8.     private String username;
    9. }

    UsernameAndPassword

    1. /**
    2.  
    3.  * @context:用户名和密码
    4.  * @params :  null
    5.  * @return :  * @return : null
    6.  */
    7. @Data
    8. @AllArgsConstructor
    9. @NoArgsConstructor
    10. public class UsernameAndPassword {
    11.     /*用户名 */
    12.     private String username;
    13.     /*密码*/
    14.     private String password;
    15. }
    • 授权服务编写

    首先创建一个 接口 IJWTService

    定义我们需要实现的授权方法

    1. /**
    2.  
    3.  * @context: JWT 相关服务接口定义
    4.  * @params :  null
    5.  * @return :  * @return : null
    6.  */
    7. public interface IJWTService {
    8.     /*
    9.      * 生成 token 使用默认的超时时间
    10.      * */
    11.     String generateToken(String username, String password) throws Exception;
    12.     /*
    13.      * 生成 JWT Token 可以设置超时时间 单位是天
    14.      * */
    15.     String generateToken(String username, String password, Integer expireTime) throws Exception;
    16.     /*
    17.      * 注册用户并且生成 token 返回
    18.      * */
    19.     String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword) throws Exception;
    20. }
    • 授权方法实现类

    这里我们有三个方法实现

    • 默认超时时间的 生成 token

    • 自定义超时时间的设置生成token

    • 注册新用户并且生成的token返回

    JWT对象生成细节:

    1) 我们需要设置需要传递的对象

    2)我们需要设置一个不重复的 id

    3)我们需要设置超时时间

    4)设置我们的加密签名

    5)完成设置返回字符串对象

    1. Jwts.builder()
    2.     //这里 claim 其实就是 jwt 的 payload 对象 --> KV
    3.     .claim(CommonCanstant.JWT_USER_INFO_KEY, JSON.toJSONString(loginUserinfo))
    4.     // jwt id 表示是 jwt的id
    5.     .setId(UUID.randomUUID().toString())
    6.     //jwt 的过期时间
    7.     .setExpiration(expireDate)
    8.     // 这里是设置加密的私钥和加密类型
    9.     .signWith(getPrivateKey(), SignatureAlgorithm.RS256)
    10.     //生成 jwt信息 返回的是一个字符串类型
    11.     .compact();
    12.     }
    • 完整代码

    1. @Service
    2. @Slf4j
    3. @Transactional(rollbackFor = Exception.class)
    4. public class IJWTServiceIpml implements IJWTService {
    5.     @Autowired
    6.     private EcommerceUserDao ecommerceUserDao;
    7.     @Override
    8.     public String generateToken(String username, String password) throws Exception {
    9.         return generateToken(username, password, 0);
    10.     }
    11.     @Override
    12.     public String generateToken(String username, String password, Integer expireTime) throws Exception {
    13.         //首先需要验证用户是否通过授权校验,即 输入的用户名和密码能否寻找到匹配数据表的记录
    14.         EcommerceUser ecommerceUser = ecommerceUserDao.findByUsernameAndPassword(username, password);
    15.         if (ecommerceUser == null) {
    16.             log.error("can not find user:[{}],[{}]", username, password);
    17.             return null;
    18.         }
    19.         //Token 中塞入对象, 即 JWT中 储存的对象,后端拿到这些信息 就可以知道那个用户在操作
    20.         LoginUserinfo loginUserinfo = new LoginUserinfo(
    21.                 ecommerceUser.getId(), ecommerceUser.getUsername()
    22.         );
    23.         if (expireTime <= 0) {
    24.             expireTime = AuthorCanstant.DEFAULT_EXPIRE_DAY;
    25.         }
    26.         //计算超时时间
    27.         ZonedDateTime zdt = LocalDate.now().plus(expireTime, ChronoUnit.DAYS)
    28.                 .atStartOfDay(ZoneId.systemDefault());
    29.         Date expireDate = Date.from(zdt.toInstant());
    30.         return Jwts.builder()
    31.                 //这里 claim 其实就是 jwt 的 payload 对象 --> KV
    32.                 .claim(CommonCanstant.JWT_USER_INFO_KEY, JSON.toJSONString(loginUserinfo))
    33.                 // jwt id 表示是 jwt的id
    34.                 .setId(UUID.randomUUID().toString())
    35.                 //jwt 的过期时间
    36.                 .setExpiration(expireDate)
    37.                 // 这里是设置加密的私钥和加密类型
    38.                 .signWith(getPrivateKey(), SignatureAlgorithm.RS256)
    39.                 //生成 jwt信息 返回的是一个字符串类型
    40.                 .compact();
    41.     }
    42.     @Override
    43.     public String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword) throws Exception {
    44.         //先去校验 用户名是否存在 如果存在 不能重复注册
    45.         EcommerceUser oldUser = ecommerceUserDao.findByUsername(usernameAndPassword.getUsername());
    46.         if (null != oldUser) {
    47.             log.error("username is registered:[{}]", oldUser.getUsername());
    48.             return null;
    49.         }
    50.         EcommerceUser ecommerceUser = new EcommerceUser();
    51.         ecommerceUser.setUsername(usernameAndPassword.getUsername());
    52.         ecommerceUser.setPassword(usernameAndPassword.getPassword()); //MD5 编码以后
    53.         ecommerceUser.setExtraInfo("{}");
    54.         //注册一个新用户 写到一个 记录表中
    55.         ecommerceUser = ecommerceUserDao.save(ecommerceUser);
    56.         log.info("regiter user success:[{}],[{}]", ecommerceUser.getUsername());
    57.         //生成 token 并且返回
    58.         return generateToken(ecommerceUser.getUsername(), ecommerceUser.getPassword());
    59.     }
    60.     /*
    61.      * 根据本地储存的私钥获取到 PrivateKey对象
    62.      * */
    63.     private PrivateKey getPrivateKey() throws Exception {
    64.         //使用给定的编码密钥创建一个新的PKCS8EncodedKeySpec。
    65.         PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(new BASE64Decoder().decodeBuffer(AuthorCanstant.PRIVATE_KEY));
    66.         // 设置生成新密钥的工厂加密方式
    67.         KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    68.         //返回生成好的密钥
    69.         return keyFactory.generatePrivate(priPKCS8);
    70.     }
    71. }

    之后我们的授权都会使用到以上的方法

    • Controller

    我们需要给注册用户和生成token 一个程序的入口

    就是我们的 AuthorityController,这里可以用到我们之前使用的注解@IgnoreResponseAdvice我们为啥那么不让他封装呢,我们需要验证,单纯的 JwtToken对象就可以了,不需要封装和转化

    1. @Slf4j
    2. @RestController
    3. @RequestMapping("/authority")
    4. public class AuthorityConroller {
    5.     private final IJWTService ljwtService;
    6.     public AuthorityConroller(IJWTService ljwtService) {
    7.         this.ljwtService = ljwtService;
    8.     }
    9.     /*
    10.      * 从授权中心 获取 token (其实就是登陆功能) 且返回信息中没有统一响应的包装
    11.      * */
    12.     @IgnoreResponseAdvice
    13.     @PostMapping("/token")
    14.     public JwtToken token(@RequestBody UsernameAndPassword usernameAndPassword) throws Exception {
    15.         //通常 日志里不会答打印用户的信息 防止泄露,我们这本身就是一个授权服务器,本身就不对外开放,所以我们可以打印用户信息到日志方便查看
    16.         log.info("request to get token with param:[{}]", JSON.toJSONString(usernameAndPassword));
    17.         return new JwtToken(ljwtService.generateToken(
    18.                 usernameAndPassword.getUsername(),
    19.                 usernameAndPassword.getPassword()));
    20.     }
    21.     /*注册用户并且返回注册当前用户的token 就是通过授权中心常见用户*/
    22.     @IgnoreResponseAdvice
    23.     @PostMapping("/register")
    24.     public JwtToken register(@RequestBody UsernameAndPassword usernameAndPassword) throws Exception {
    25.         log.info("register user with param:[{}]", JSON.toJSONString(usernameAndPassword));
    26.         return new JwtToken(ljwtService.registerUserAndGenerateToken(usernameAndPassword));
    27.     }
    28. }

    鉴权编码实现

    这里我们打鉴权 放到公共模块里 为什么呢,这里我们不止是鉴权中心还有其他的服务也要用到鉴权服务,秉着封装的思想,我们提取公共的方法放到 Common里面

    创建JWT Token解析类TokenParseUtil

    1. /**
    2.  
    3.  * @context: JWT Token 解析工具类
    4.  * @params :  null
    5.  * @return :  * @return : null
    6.  */
    7. public class TokenParseUtil {
    8.     public static LoginUserinfo parseUserInfoFromToken(String token) throws Exception {
    9.         if (null == token) {
    10.             return null;
    11.         }
    12.         Jws claimsJws = parseToken(token, getPublicKey());
    13.         Claims body = claimsJws.getBody();
    14.         //如果 Token 已经过期返回 null
    15.         if (body.getExpiration().before(Calendar.getInstance().getTime())) {
    16.             return null;
    17.         }
    18.         //     返回 Token中保存的用户信息
    19.         return JSON.parseObject(
    20.                 body.get(CommonCanstant.JWT_USER_INFO_KEY).toString(), LoginUserinfo.class
    21.         );
    22.     }
    23.     /*
    24.      * 通过公钥去解析 JWT Token
    25.      * */
    26.     private static Jws parseToken(String token, PublicKey publicKey) {
    27.         // 用设置签名公钥,解析claims信息 token
    28.         return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    29.     }
    30.     /*
    31.      * 根据本地存储的公钥获取到 getPublicKey
    32.      * */
    33.     public static PublicKey getPublicKey() throws Exception {
    34.         //解码器 我们设置解码器 将公钥放进去
    35.         X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
    36.                 new BASE64Decoder().decodeBuffer(CommonCanstant.PUBLIC_KEY)
    37.         );
    38.         //创建 RSA 实例 通过示例生成公钥对象
    39.         return KeyFactory.getInstance("RSA").generatePublic(keySpec);
    40.     }
    41. }

    这里是涉及到一个问题 ,token要是传输的不是jwt token对象,会跑出异常,没有兜底,

    其实这里这问题其实也不成立,应为你没有传入token对象,我们这里抛出异常是正确的,也不会影响其他服务,之后搭配sentinel和豪猪哥 可以实现异常重启等等,这里我们就先不编写兜底方法,以解析jwt token为主。

    验证鉴权授权

    我们写一个 test 类来测试 授权和鉴权拿到对象,是否有效

    1. /**
    2.  
    3.  * @context: JWT 相关测试类
    4.  * @params :  null
    5.  * @return :  * @return : null
    6.  */
    7. @Slf4j
    8. @SpringBootTest
    9. @RunWith(SpringRunner.class)
    10. public class JWTServiceTest {
    11.     @Autowired
    12.     private IJWTService ijwtService;
    13.     @Test
    14.     public void testGenerateAndParseToken() throws Exception {
    15.         String jwtToken = ijwtService.generateToken(
    16.                 "hyc@qq.com""e10adc3949ba59abbe56e057f20f883e"
    17.         );
    18.         log.info("jwt token is:[{}]", jwtToken);
    19.         LoginUserinfo userinfo = TokenParseUtil.parseUserInfoFromToken(jwtToken);
    20.         log.info("userinfo by jwt prase token :[{}]", JSON.toJSONString(userinfo));
    21.     }
    22. }

    启动测试查看结果

    1. eyJhbGciOiJSUzI1NiJ9
    2. .eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjExLFwidXNlcm5hbWVcIjpcImh5Y0BxcS5jb21cIn0iLCJqdGkiOiIzNDgwNjdjMi00MTBlLTQ3MjItYmM3ZS02NWQyYmNmYTRkN2MiLCJleHAiOjE2Mzg3MjAwMDB9
    3. .ZbFl81MkIipJSULZLf4F2X2Fb0q1TwhHIMT7nyZsZVwUxXyZnK54RlzoGM_b-kMUdKO_Tab-qEeOT6Jn--FiKmbOziWXiBx3a-k5ipthMJx0Fez-X8Acty-Pg7zukNalugiLxGb5ophQoVQWRTDmv2hytGHqiV71HVyErznkJa36QQr6QsjXqlJleo3BBt-6BFzdTFPLUmdTEJ4XsmZBa_acUDGBhY0_tU2gYtKBWhwvMCknuyCcV-_GVI5EvgMIKRpeFSZrWfTsDG2y1MFcyzjKE6jnzek-YwT3XkzQ8eGzUbiOlaU_Zx5OJah-UtrKwqlAw9WbO71pNgEBefdsYw

    这是封装好的 JWT Token 这里我们可以看到三个点分别分割 了 header和payload以及签名,和我们之前讲的 结构一模一样,

    userinfo by jwt prase token :[{"id":11,"username":"hyc@qq.com"}]
    

    获取到的我们放在 jwt 里面需要传递的对象

    验证对外提供的接口是否好用

    这里我们编写 http脚本来测试对外题提供的接口是否有用

    • Token 方法

    1. ### 获取 Token -- 登录功能实现
    2. POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token
    3. Content-Type: application/json
    4. {
    5.   "username""hyc@qq.com",
    6.   "password""e10adc3949ba59abbe56e057f20f883e"
    7. }
    1. POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token
    2. HTTP/1.1 200 
    3. Content-Type: application/json
    4. Transfer-Encoding: chunked
    5. Date: Sun, 05 Dec 2021 15:35:52 GMT
    6. Keep-Alive: timeout=60
    7. Connection: keep-alive
    8. {
    9.   "token""eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjExLFwidXNlcm5hbWVcIjpcImh5Y0BxcS5jb21cIn0iLCJqdGkiOiIxNDU1M2FjZi1lZmE5LTQ4OTgtOTliYS1hNzA4NWI4MjU4MzAiLCJleHAiOjE2Mzg3MjAwMDB9.AlOpo6uf97R20ZLojXeun-3MK8DpSYlWxEygvDrtQeWaM9R0iKx-iW1VXnK6WoEntvqPxIrmPA7khjl3dXPa8kQHtdq-LVO7BDuZZDiQyZ64ZS7A9jWZr5JReSWBUSR1YUnsOvBRMkx4JVcAF3_W7nHwd722FFzOZRCr72hLHQIKpsugKtqjMEtaiEW0vcqphCYRJTAO_rQx1Lb1eVVg_Ufur0qSlKkV5dSJ0x3x9mc9UZRckwN0rrP7wQxZcrxJvKTfX7CkRRSO-CxZbG4WLokSaMtaGBMWU-7KGq7HSCZ0yuOgbbLdouHncsp6VD2tNLFdWSdJ_whCIbZxfX8R7w"
    10. }

    获取 token 成功

    这里他没有被响应包裹,证明我们之前的选择屏蔽注解也生效了,很符合我们的预期

    • 验证如果记录数据表没有是否会返回null

    1. ### 获取 Token -- 登录功能实现
    2. POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token
    3. Content-Type: application/json
    4. ### 随便写的id
    5. {
    6. "username""hyc1111@qq.com",
    7. "password""e10adc3949ba59abbe56e057f20f883e"
    8. }

    返回结果 也符合我们预期 是 null

    1. POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token
    2. HTTP/1.1 200 
    3. Content-Type: application/json
    4. Transfer-Encoding: chunked
    5. Date: Sun, 05 Dec 2021 15:40:44 GMT
    6. Keep-Alive: timeout=60
    7. Connection: keep-alive
    8. {
    9.   "token"null
    10. }
    • register

    1. ### 注册用户并返回 Token -- 注册功能实现
    2. POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register
    3. Content-Type: application/json
    4. {
    5.   "username""hyc@qq.com",
    6.   "password""e10adc3949ba59abbe56e057f20f883e"
    7. }

    这个用户之前是注册过的,我们来看一下是否会返回我们预期的处理

    1. POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register
    2. HTTP/1.1 200 
    3. Content-Type: application/json
    4. Transfer-Encoding: chunked
    5. Date: Sun, 05 Dec 2021 15:42:00 GMT
    6. Keep-Alive: timeout=60
    7. Connection: keep-alive
    8. {
    9.   "token"null
    10. }
    • 现在我们去注册一个新的用户

    1. ### 注册用户并返回 Token -- 注册功能实现
    2. POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register
    3. Content-Type: application/json
    4. {
    5.   "username""hyc11@qq.com",
    6.   "password""e10adc3949ba59abbe56e057f20f883e"
    7. }

    符合预期结果,创建了我们预期的对象,这个时候我们去看一下数据表

    1. POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register
    2. HTTP/1.1 200 
    3. Content-Type: application/json
    4. Transfer-Encoding: chunked
    5. Date: Sun, 05 Dec 2021 15:42:57 GMT
    6. Keep-Alive: timeout=60
    7. Connection: keep-alive
    8. {
    9.   "token""eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEyLFwidXNlcm5hbWVcIjpcImh5YzExQHFxLmNvbVwifSIsImp0aSI6IjMxNDc0NmIwLTMyOGYtNDZkNS05ZTIwLTg3YjI0OWY1ZjZkOCIsImV4cCI6MTYzODcyMDAwMH0.MKxk-Q4BG5kaYFAsLiy13trtk_gDFmCKORpdE4EAwgSVecXFQcYfT1VvqSAKvoQLFsSlQAxOR5elV8CFOoKwAomwqdyyghZp63NKJ2smRbg3Y-4jWBzFVsUgcjOY2fwh7oNTdHEsWmLBYAh5r0hm_MysZsUEsE-cwb3sw8NSMk1OZp0J6tcRras7V1Uw5xXH8OnCoq2cUfdynJMHS29EzJT1TFPb8unVQ_A1RWodsHdK3n1Bl4wFbJjMtnHx7vzOeAUSNJx1XpAGdo0xYHK6HBpS9E1KBS3x1AnYFONM0DKd4-_QxMkBW1kkg2uWrRpf3GYZF20FKxXgmBAPHGZhew"
    10. }

    对象生成,功能验证一切正常

    鉴权服务中心总结

    对比基于Token与基于服务器的身份认证

    传统:

    • 最为传统的做法,客户端储存 cookie 一般是 Session id 服务器存储 Session

    • Session 是每次用户认证通过以后 ,服务器需要创建一条记录保存用户信息,通常是在内存中(也可以放在redis中),随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大

    • 不同域名之前切换的时候,请求可能会被禁止,即跨越问题

    基于token:

     

    • JWT与Session的差异相同点是,他们都是存储用户信息。然而Session是在服务器端的,而JWT是在客户端的

    • JWT方式将用户状态分散到了客户端中,可以明显减轻请服务器的内存压力,服务端只需要用算法解析客户端的token就可以得到信息

    • 两者优缺点的对比

    • 解析方法:JWT使用算法直接解析得到用户信息;Session需要额外的数据映射。实现匹配

    • 管理方法:JWT只有过期时间的限制,Session 数据保存在服务器,可控性更强

    • 跨平台:JWT就是一段字符串,可以任意传播,Session跨平台需要有统一的解析平台,较为繁琐

    • 时效性:JWT一旦生成 独立存在,很难做到特殊的控制;Session时效性完全由服务端的逻辑说了算

    TIPS :各自都有优缺点,都是登陆和授权的解决方案


    推荐3个原创springboot+Vue项目,有完整视频讲解与文档和源码:

    【dailyhub】【实战】带你从0搭建一个Springboot+elasticsearch+canal的完整项目

    • 视频讲解:https://www.bilibili.com/video/BV1Jq4y1w7Bc/

    • 完整开发文档:https://www.zhuawaba.com/post/124

    • 线上演示:https://www.zhuawaba.com/dailyhub

    【VueAdmin】手把手教你开发SpringBoot+Jwt+Vue的前后端分离后台管理系统

    • 视频讲解:https://www.bilibili.com/video/BV1af4y1s7Wh/

    • 完整开发文档前端:https://www.zhuawaba.com/post/18

    • 完整开发文档后端:https://www.zhuawaba.com/post/19

    • 线上演示:https://www.markerhub.com/vueadmin/

    【VueBlog】基于SpringBoot+Vue开发的前后端分离博客项目完整教学

    • 视频讲解:https://www.bilibili.com/video/BV1PQ4y1P7hZ

    • 完整开发文档:https://www.zhuawaba.com/post/17

  • 相关阅读:
    【C++杂货铺】一文带你走进RBTree
    艺术大观杂志艺术大观杂志社艺术大观编辑部2022年第20期目录
    JSON Web Token
    Jenkins安装配置及插件安装使用
    matlab-BP神经网络的训练参数大全
    nRF52832——大量数据传输时导致蓝牙断开连接,且无法被搜索到的解决方案(广播参数的设置、程序设计方法)
    创新案例|实现YouTube超速增长的3大敏捷组织运营机制(上)
    全场景 MPP 数据库ERM StarRocks 源代码数据湖分析
    几类单波束和多波束声呐的区别
    ArmSoM-W3之RK3588 MPP环境配置
  • 原文地址:https://blog.csdn.net/qq_45637260/article/details/126133713