鉴权中心服务
认识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)+<span>"."</span>+base64UrlEncode(payload),secret)
产生一个签名,返回一个字符串,返回给客户端,之后客户端每次访问都要带上这个字符串,进行鉴权
JWT使用 .
号来连接 HHH.PPPP.SSSS
授权,鉴权设计
这里我们先不考虑 gateway 网关,后续会搭建,我们的重点放在中间和右边部分
鉴权部分,我们独立实现公共的工具类,为什么?以下三点
- JWT本质上是通过算法算出的加密字符串,也可以通过算法反向解析出来,他不依赖任何的框架,所以这个功能有可以单独提取出来的前提
- 我们的电商系统包含多个微服务,很显然我们每个服务都需要鉴权,于是我们把这个方法提取出来,方便复用
- 高性能鉴权,为什么不在授权中心做鉴权,首先他回头过http请求等一系列操作,我们在本地只用java的话 少去了很多步骤,性能得到倍数的增长
授权编码实现
我们创建新的一个服务来编写我们的鉴权中心
e-commerce-authority-center
导入相关的依赖
<span><<span>dependencies</span>></span><br>    <br>    <span><<span>dependency</span>></span><br>        <span><<span>groupId</span>></span>com.alibaba.cloud<span>groupId</span>><br>        <span><<span>artifactId</span>></span>spring-cloud-starter-alibaba-nacos-discovery<span>artifactId</span>><br>        <span><<span>version</span>></span>2.2.3.RELEASE<span>version</span>><br>    <span>dependency</span>><br>    <br>    <span><<span>dependency</span>></span><br>        <span><<span>groupId</span>></span>org.springframework.boot<span>groupId</span>><br>        <span><<span>artifactId</span>></span>spring-boot-starter-data-jpa<span>artifactId</span>><br>    <span>dependency</span>><br>    <br>    <span><<span>dependency</span>></span><br>        <span><<span>groupId</span>></span>mysql<span>groupId</span>><br>        <span><<span>artifactId</span>></span>mysql-connector-java<span>artifactId</span>><br>        <span><<span>version</span>></span>8.0.12<span>version</span>><br>        <span><<span>scope</span>></span>runtime<span>scope</span>><br>    <span>dependency</span>><br>    <span><<span>dependency</span>></span><br>        <span><<span>groupId</span>></span>com.hyc.ecommerce<span>groupId</span>><br>        <span><<span>artifactId</span>></span>e-commerce-mvc-config<span>artifactId</span>><br>        <span><<span>version</span>></span>1.0-SNAPSHOT<span>version</span>><br>    <span>dependency</span>><br>    <br>    <span><<span>dependency</span>></span><br>        <span><<span>groupId</span>></span>org.springframework.cloud<span>groupId</span>><br>        <span><<span>artifactId</span>></span>spring-cloud-starter-zipkin<span>artifactId</span>><br>    <span>dependency</span>><br>    <span><<span>dependency</span>></span><br>        <span><<span>groupId</span>></span>org.springframework.kafka<span>groupId</span>><br>        <span><<span>artifactId</span>></span>spring-kafka<span>artifactId</span>><br>        <span><<span>version</span>></span>2.5.0.RELEASE<span>version</span>><br>    <span>dependency</span>><br>    <br>    <span><<span>dependency</span>></span><br>        <span><<span>groupId</span>></span>org.freemarker<span>groupId</span>><br>        <span><<span>artifactId</span>></span>freemarker<span>artifactId</span>><br>        <span><<span>version</span>></span>2.3.30<span>version</span>><br>    <span>dependency</span>><br>    <span><<span>dependency</span>></span><br>        <span><<span>groupId</span>></span>cn.smallbun.screw<span>groupId</span>><br>        <span><<span>artifactId</span>></span>screw-core<span>artifactId</span>><br>        <span><<span>version</span>></span>1.0.3<span>version</span>><br>    <span>dependency</span>><br><span>dependencies</span>>
导入好依赖之后我们 编写对应的配置,如注册到naocs 加入adminserver的监管,配置数据源等 这里我们使用jpa 来做orm
- *配置编写
<span>server:</span><br>  <span>port:</span> <span>7000</span><br>  <span>servlet:</span><br>    <span>context-path:</span> <span>/ecommerce-authority-center</span><br><br><span>spring:</span><br>  <span>application:</span><br>    <span>name:</span> <span>e-commerce-authority-center</span><br>  <span>cloud:</span><br>    <span>nacos:</span><br>      <span>discovery:</span><br>        <span>enabled:</span> <span>true</span> <span># 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可</span><br>        <span>server-addr:</span> <span>127.0</span><span>.0</span><span>.1</span><span>:8848</span> <span># Nacos 服务器地址</span><br>        <span># server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址</span><br>        <span>namespace:</span> <span>1bc13fd5-843b-4ac0-aa55-695c25bc0ac6</span><br>        <span>metadata:</span><br>          <span>management:</span><br>            <span>context-path:</span> <span>${server.servlet.context-path}/actuator</span><br>  <span>jpa:</span><br>    <span>show-sql:</span> <span>true</span><br>    <span>hibernate:</span><br>      <span>ddl-auto:</span> <span>none</span><br>    <span>properties:</span><br>      <span>hibernate.show_sql:</span> <span>true</span><br>      <span>hibernate.format_sql:</span> <span>true</span><br>    <span>open-in-view:</span> <span>false</span><br>  <span>datasource:</span><br>    <span># 数据源</span><br>    <span>url:</span> <span>jdbc:mysql://127.0.0.1:3306/imooc_e_commerce?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC</span><br>    <span>username:</span> <span>root</span><br>    <span>password:</span> <span>root</span><br>    <span>type:</span> <span>com.zaxxer.hikari.HikariDataSource</span><br>    <span>driver-class-name:</span> <span>com.mysql.cj.jdbc.Driver</span><br>    <span># 连接池</span><br>    <span>hikari:</span><br>      <span>maximum-pool-size:</span> <span>8</span><br>      <span>minimum-idle:</span> <span>4</span><br>      <span>idle-timeout:</span> <span>30000</span><br>      <span>connection-timeout:</span> <span>30000</span><br>      <span>max-lifetime:</span> <span>45000</span><br>      <span>auto-commit:</span> <span>true</span><br>      <span>pool-name:</span> <span>ImoocEcommerceHikariCP</span><br>  <span>kafka:</span><br>    <span>bootstrap-servers:</span> <span>127.0</span><span>.0</span><span>.1</span><span>:9092</span><br>    <span>producer:</span><br>      <span>retries:</span> <span>3</span><br>    <span>consumer:</span><br>      <span>auto-offset-reset:</span> <span>latest</span><br>  <span>zipkin:</span><br>    <span>sender:</span><br>      <span>type:</span> <span>kafka</span> <span># 默认是 web</span><br>    <span>base-url:</span> <span>http://127.0.0.1:9411/</span><br><br><span># 暴露端点</span><br><span>management:</span><br>  <span>endpoints:</span><br>    <span>web:</span><br>      <span>exposure:</span><br>        <span>include:</span> <span>'*'</span><br>  <span>endpoint:</span><br>    <span>health:</span><br>      <span>show-details:</span> <span>always</span>
配置完成之后,编写主启动类 @EnableJpaAuditing
因为我们用到 自动加入创建时间和修改时间,所以我们需要打开 jpa的自动审计功能,不然会报错
<span>@EnableJpaAuditing</span> <span>//允许 jpa 的自动审计</span><br><span>@SpringBootApplication</span><br><span>@EnableDiscoveryClient</span><br><span>public</span> <span><span>class</span> <span>AuthorityApplication</span> </span>{<br>    <span><span>public</span> <span>static</span> <span>void</span> <span>main</span><span>(String[] args)</span> </span>{<br>        SpringApplication.run(AuthorityApplication<span>.<span>class</span>, <span>args</span>)</span>;<br><br>    }<br>}
test包下就测试环境是否正确
<span>/**<br> * 授权中心测试入口<br> * 验证授权中心 环境可用性<br> */</span><br><span>@SpringBootTest</span><br><span>@RunWith</span>(SpringRunner<span>.<span>class</span>)<br><span>public</span> <span>class</span> <span>AuthorityCenterApplicationTest</span> </span>{<br><br>    <span>@Test</span><br>    <span><span>public</span> <span>void</span> <span>conetextLoad</span><span>()</span> </span>{<br><br>    }<br>}
环境ok之后
我们去测试 数据库操作是否可用
编写实体类ecommerceUser
<span>/*<br> * 用户表实体类定义<br> * */</span><br><span>@Entity</span><br><span>@EntityListeners</span>(AuditingEntityListener<span>.<span>class</span>)<br>@<span>Table</span>(<span>name</span> </span>= <span>"t_ecommerce_user"</span>)<br><span>@Data</span><br><span>@NoArgsConstructor</span><br><span>@AllArgsConstructor</span><br><span>public</span> <span><span>class</span> <span>EcommerceUser</span> </span>{<br>    <span>/* 自增组件*/</span><br>    <span>@Id</span><br>    <span>@GeneratedValue</span>(strategy = GenerationType.IDENTITY)<br>    <span>@Column</span>(name = <span>"id"</span>, nullable = <span>false</span>)<br>    <span>private</span> <span>long</span> id;<br><br>    <span>/*用户名*/</span><br>    <span>@Column</span>(name = <span>"username"</span>, nullable = <span>false</span>)<br><br>    <span>private</span> String username;<br><br>    <span>/* MD5 密码*/</span><br>    <span>@Column</span>(name = <span>"password"</span>, nullable = <span>false</span>)<br>    <span>private</span> String password;<br><br>    <span>/*额外的信息 json 字符串存储*/</span><br>    <span>@Column</span>(name = <span>"extra_info"</span>, nullable = <span>false</span>)<br>    <span>private</span> String extraInfo;<br><br>    <span>/*自动加入创建时间 需要主启动类的注解*/</span><br>    <span>@CreatedDate</span><br>    <span>@Column</span>(name = <span>"create_time"</span>, nullable = <span>false</span>)<br>    <span>private</span> Date createTime;<br><br>    <span>/*自动加入更新时间 需要主启动类的注解*/</span><br>    <span>@CreatedDate</span><br>    <span>@Column</span>(name = <span>"update_time"</span>, nullable = <span>false</span>)<br>    <span>private</span> Date updateTime;<br>}
有了实体类我们需要有数据操作的实现 于是编写Dao 接口
其实当我们创建接口的时候jpa就已经有了对应的基础增删改查的方法
这里我们实现两个自定义查询方法
<span>/**<br> * EcommerceUserDao 接口定义<br> */</span><br><span>public</span> <span><span>interface</span> <span>EcommerceUserDao</span> <span>extends</span> <span>JpaRepository</span><<span>EcommerceUser</span>, <span>Long</span>> </span>{<br><br>    <span>/*<br>     * 根据用户名查询 EcommerceUser 对象<br>     * 等于 select * form t_ecommerce_user where username=?<br>     * */</span><br>    <span>EcommerceUser <span>findByUsername</span><span>(String name)</span></span>;<br><br>    <span>/*<br>     * 根据用户名查询 EcommerceUser 对象<br>     * 等于 select * form t_ecommerce_user where username=? and password=?<br>     * */</span><br>    <span>EcommerceUser <span>findByUsernameAndPassword</span><span>(String name, String password)</span></span>;<br><br>}
之后创建 test service
<span>/**<br> * <span>@author</span> : 冷环渊<br> * <span>@date</span> : 2021/12/4<br> * <span>@context</span>: EcommerceUser 相关测试<br> * <span>@params</span> :  null<br> * <span>@return</span> :  * <span>@return</span> : null<br> */</span><br><span>@SpringBootTest</span><br><span>@RunWith</span>(SpringRunner<span>.<span>class</span>)<br>@<span>Slf4j</span><br><span>public</span> <span>class</span> <span>EcommerUserTest</span> </span>{<br>    <span>@Autowired</span><br>    EcommerceUserDao ecommerceUserDao;<br><br>    <span>/*测试  新增一个用户数据 */</span><br>    <span>@Test</span><br>    <span><span>public</span> <span>void</span> <span>createUserRecord</span><span>()</span> </span>{<br>        EcommerceUser ecommerceUser = <span>new</span> EcommerceUser();<br>        <span>//设置要插入的信息</span><br>        ecommerceUser.setUsername(<span>"hyc@qq.com"</span>);<br>        ecommerceUser.setPassword(MD5.create().digestHex(<span>"123456"</span>));<br>        ecommerceUser.setExtraInfo(<span>"{}"</span>);<br>        <span>//日志打印返回结果</span><br>        log.info(<span>"server user:[{}]"</span>, JSON.toJSON(ecommerceUserDao.save(ecommerceUser)));<br>    }<br><br>    <span>/*测试 我们编写的自定义方法 查询 刚才创建的新角色*/</span><br>    <span>@Test</span><br>    <span><span>public</span> <span>void</span> <span>SelectUserInfo</span><span>()</span> </span>{<br>        String username = <span>"hyc@qq.com"</span>;<br>        log.info(<span>"select userinof:[{}]"</span>, JSON.toJSON(ecommerceUserDao.findByUsername(username)));<br>    }<br>}
测试相关的 方法 新增用户啊 或者是 按条件查询用户 ,测试均通过
生成RSA256的公钥 和 私钥 非对称加密算法
他通过 私钥加密 公钥解密来完成验证,目前很多的鉴权 都是 JWTRSA256的算法来加密鉴权的,如果了解不多,就是用RSA256就可以了
- *编码
编写生成公钥密钥的测试类,创建 一些我们常用的VO对象 用来储存我们常用的一些变量,比如用户信息,公钥,密钥,一些常用的属性 放进 VO的模型里
<span>@Slf</span>4j<br><span>@SpringBootTest</span><br><span>@RunWith</span>(SpringRunner<span>.<span>class</span>)<br>/**<br> *<br> * @<span>author</span> : 冷环渊<br> * @<span>date</span> : 2021/12/5<br> * @<span>context</span>: <span>RSA</span> 非对称 加密算法<br> * @<span>params</span> :  <span>null</span> <br> * @<span>return</span> :  * @<span>return</span> : <span>null</span><br> */<br><span>public</span> <span>class</span> <span>RSATest</span> </span>{<br>    <span>@Test</span><br>    <span><span>public</span> <span>void</span> <span>generateKeyBytes</span><span>()</span> <span>throws</span> Exception </span>{<br>        <span>/*获取到 RSA算法实例*/</span><br>        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(<span>"RSA"</span>);<br>        <span>/* 这里最小是 2048 低于的话 是会报错的*/</span><br>        keyPairGenerator.initialize(<span>2048</span>);<br>        <span>/*<br>         * 生成公钥对<br>         * */</span><br>        KeyPair keyPair = keyPairGenerator.generateKeyPair();<br>        <span>/*获取 公钥和私钥对象*/</span><br>        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();<br>        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();<br>        log.info(<span>"private key:[{}]"</span>, Base64.encode(privateKey.getEncoded()));<br>        log.info(<span>"public key:[{}]"</span>, Base64.encode(publicKey.getEncoded()));<br>    }<br>}
- *创建VO对象保存 我们常用且不会变化的值和对象
存储私钥 应为是私钥 所以只对鉴权中心 暴露 于是我们在鉴权服务中创建Constant包创建这个AuthotityConstant类保存信息
<span>/**<br> * <span>@author</span> : 冷环渊<br> * <span>@date</span> : 2021/12/5<br> * <span>@context</span>: 鉴权的常量<br> * <span>@params</span> :  null<br> * <span>@return</span> :  * <span>@return</span> : null<br> */</span><br><span>public</span> <span><span>class</span> <span>AuthorCanstant</span> </span>{<br>    <span>/*私钥 只暴露给 鉴权中心 不暴露给任何的其他服务*/</span><br>    <span>public</span> <span>static</span> <span>final</span> String PRIVATE_KEY = <span>"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBA"</span> +<br>            <span>"QCMXrQCudalKHJlH16YHr9mI5/xyYnkp5u2gAbMFf2xAHAyykYmixJP3CqG2a8tUwiJjjTIJXP+79Jzgjgg"</span> +<br>            <span>"VbBaTakrvjeFXz9HNP1D4XD6Li+sRVjnN1iBUwIFRxiFN2EOJflA9bqeQLAge/LgAu06y3jdLLleJF7yDRuMH"</span> +<br>            <span>"YedqPl9AJa5RdJmt0OgCoVOqacB7oGkFCFISm0Cwjfgq06nyiiULGZNVt8uhDxZAE4Pi2lmf3yggXCBH9AtU/2"</span> +<br>            <span>"XdyxU9caQJOAbYGxd/mART/NivBjSqo60wcBnktI+booUbDKRBbWRxvfYqKWEwPOwxlJUB3l3pcLZm866Xl3qtVM"</span> +<br>            <span>"XAgMBAAECggEADCGjLRkik+OK/3JWmo8Nu6YYjKz+XeSecIdgDwNXiZSgHcOdjHc4fe5pPn5RxXkHo9vGdAXIoJ/Z"</span> +<br>            <span>"cGIwt5qwQx2zITSvV7eDoIPT36n8OaMEO79Cj7kYzRR/eDVMyTagDLj7ccHK/yJYFnaf5vxZxFsRdwwGeTxreD"</span> +<br>            <span>"/pwZJLxjRSz1W57v5yUJNPPimNB229EogNYHIhQ8+Z7OGiilbtBIL9r6lqlz2hUAVBzXl4kOXFVI+vEodLuV2"</span> +<br>            <span>"rtQXXrpO1+AgH5lZJ7ahShKbqHt/Q6uJSTKAhbsfv/iadcPjmYp2F7nnYBLf66Jln6AWUwnXrJ7XETOf/+Qcib"</span> +<br>            <span>"q/5m6RjAQKBgQDruxn+kaDr5uYQMVSHog+CBRBJghJ4JklhY7ZDYJ2wN2KNHOd3mW/wUVDihVIyRFniIzsWU"</span> +<br>            <span>"0lnI+4OLqNLAZOBaQB5VrjyH4fxn5b26t0xLO1d5EWcOYI8ZRhwWDWaZipe2dUMeqVVMYFeDdTdNsyGrf8x"</span> +<br>            <span>"L+OVyRDiH4s4pBIs7QKBgQCYcIVFgDbrmwsP7lA9/dU9kClutY3gjEUgB2IJp2Y8S4Xhfi4NC8GqRQoMUyuqg"</span> +<br>            <span>"vPHKEiTCa1EojGHS/+r4JVcSg9Wsv64SpGZ+gANxRhfYFPrbkjU4YOMaZeCGUfKR2QnD20c3I4gdQ9kU5nK52n+Y"</span> +<br>            <span>"JEkAFUejg1Mhb6Fp6HDkwKBgAHYYBa3CxxtnUVpLXE2Woq5AWyh4QUhv5dMkYOrgPB9Ln9OR52PDOpDqK9tP"</span> +<br>            <span>"bx4/n8fqXm+QyfUhyuDP/H5XC86JC/O9vmmN4kzp5ndMsgMwvrmK4lShet1GyDd/+VqgVBmwh0r5JlrHske"</span> +<br>            <span>"sJjesfEn8YRwDIcCoOg0OQHDfwTtAoGAQfE61YvXNihFqsiOkaKCYjVAlxGWpDJJnMdU05REl4ScD6WDy"</span> +<br>            <span>"kTxq/RdmmNIGmS3i8mTS3f+Khh3kG2B1ho6wkePRxP7OEGZpqAM8ef22RtUch2tB9neDBmJXtAMzCYB3xu/O"</span> +<br>            <span>"aL3IHdDB0Va2/krUsz3PDmgmK0ed6HLfwm64l0CgYB+iGkMAQEwqYmcCEXKK825Q9y/u8PE9y8uaMGfsZQzDo6v"</span> +<br>            <span>"V5v+reOhmZRrk5BnX+pgztbE28sS6c2vYR0RYoR90aD2GXungCPXWEMDQudHFxvSsNTCYkDynjTSlnzu9aDcfqw1"</span> +<br>            <span>"UIzHog2zCquSro7tnbOMsvV5UdsLBq+WNQGgAw=="</span>;<br><br>    <span>/*默认的 token 超时时间,一天*/</span><br>    <span>public</span> <span>static</span> <span>final</span> Integer DEFAULT_EXPIRE_DAY = <span>1</span>;<br>}
之后是创建一些公共常用的VO模型 e-commerce-common
- *保存 公钥到公用包 以后我们的服务 需要做授权都需要使用到
<span>/**<br> * <span>@author</span> : 冷环渊<br> * <span>@date</span> : 2021/12/5<br> * <span>@context</span>: 通用模块的常量定义<br> * <span>@params</span> :  null<br> * <span>@return</span> :  * <span>@return</span> : null<br> */</span><br><span>public</span> <span><span>class</span> <span>CommonCanstant</span> </span>{<br>    <span>/* RSA 公钥*/</span><br>    <span>public</span> <span>static</span> <span>final</span> String PUBLIC_KEY = <span>"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjF60ArnWpShyZ"</span> +<br>            <span>"R9emB6/ZiOf8cmJ5KebtoAGzBX9sQBwMspGJosST9wqhtmvLVMIiY40yCVz/u/Sc4I4IFWwWk2pK743hV8/RzT9Q+F"</span> +<br>            <span>"w+i4vrEVY5zdYgVMCBUcYhTdhDiX5QPW6nkCwIHvy4ALtOst43Sy5XiRe8g0bjB2Hnaj5fQCWuUXSZrdDoAqFTqmnA"</span> +<br>            <span>"e6BpBQhSEptAsI34KtOp8oolCxmTVbfLoQ8WQBOD4tpZn98oIFwgR/QLVP9l3csVPXGkCTgG2BsXf5gEU/zYrwY0qqO"</span> +<br>            <span>"tMHAZ5LSPm6KFGwykQW1kcb32KilhMDzsMZSVAd5d6XC2ZvOul5d6rVTFwIDAQAB"</span>;<br><br>    <span>/* JWT 中 存储用户信息到 key*/</span><br>    <span>public</span> <span>static</span> <span>final</span> String JWT_USER_INFO_KEY = <span>"e-commerce-user"</span>;<br>    <span>/*授权中心的 service-id*/</span><br>    <span>public</span> <span>static</span> <span>final</span> String AUTHORITY_CENTER_SERVICE_ID = <span>"e-commerce-authity-center"</span>;<br>}
- *用户信息的常用VO对象
JwtToken
<span>/**<br> * <span>@author</span> : 冷环渊<br> * <span>@date</span> : 2021/12/5<br> * <span>@context</span>: 授权中心 鉴权 之后给客户端的token<br> * <span>@params</span> :  null<br> * <span>@return</span> :  * <span>@return</span> : null<br> */</span><br><span>@Data</span><br><span>@NoArgsConstructor</span><br><span>@AllArgsConstructor</span><br><span>public</span> <span><span>class</span> <span>JwtToken</span> </span>{<br>    <span>/* JWT*/</span><br>    <span>private</span> String token;<br><br><br>}
LoginUserinfo
<span>@Data</span><br><span>@NoArgsConstructor</span><br><span>@AllArgsConstructor</span><br><span>public</span> <span><span>class</span> <span>LoginUserinfo</span> </span>{<br>    <span>/*用户 id*/</span><br>    <span>private</span> Long id;<br>    <span>/*用户名*/</span><br>    <span>private</span> String username;<br>}
UsernameAndPassword
<span>/**<br> * <span>@author</span> : 冷环渊<br> * <span>@date</span> : 2021/12/5<br> * <span>@context</span>:用户名和密码<br> * <span>@params</span> :  null<br> * <span>@return</span> :  * <span>@return</span> : null<br> */</span><br><span>@Data</span><br><span>@AllArgsConstructor</span><br><span>@NoArgsConstructor</span><br><span>public</span> <span><span>class</span> <span>UsernameAndPassword</span> </span>{<br>    <span>/*用户名 */</span><br>    <span>private</span> String username;<br>    <span>/*密码*/</span><br>    <span>private</span> String password;<br>}
- *授权服务编写
首先创建一个 接口 IJWTService
定义我们需要实现的授权方法
<span>/**<br> * <span>@author</span> : 冷环渊<br> * <span>@date</span> : 2021/12/5<br> * <span>@context</span>: JWT 相关服务接口定义<br> * <span>@params</span> :  null<br> * <span>@return</span> :  * <span>@return</span> : null<br> */</span><br><span>public</span> <span><span>interface</span> <span>IJWTService</span> </span>{<br><br>    <span>/*<br>     * 生成 token 使用默认的超时时间<br>     * */</span><br>    <span>String <span>generateToken</span><span>(String username, String password)</span> <span>throws</span> Exception</span>;<br><br>    <span>/*<br>     * 生成 JWT Token 可以设置超时时间 单位是天<br>     * */</span><br>    <span>String <span>generateToken</span><span>(String username, String password, Integer expireTime)</span> <span>throws</span> Exception</span>;<br><br><br>    <span>/*<br>     * 注册用户并且生成 token 返回<br>     * */</span><br>    <span>String <span>registerUserAndGenerateToken</span><span>(UsernameAndPassword usernameAndPassword)</span> <span>throws</span> Exception</span>;<br>}
- *授权方法实现类
这里我们有三个方法实现
- 默认超时时间的 生成 token
- 自定义超时时间的设置生成token
- 注册新用户并且生成的token返回
JWT对象生成细节:
1) 我们需要设置需要传递的对象
2)我们需要设置一个不重复的 id
3)我们需要设置超时时间
4)设置我们的加密签名
5)完成设置返回字符串对象
Jwts.builder()<br>    <span>//这里 claim 其实就是 jwt 的 payload 对象 --> KV</span><br>    .claim(CommonCanstant.JWT_USER_INFO_KEY, JSON.toJSONString(loginUserinfo))<br>    <span>// jwt id 表示是 jwt的id</span><br>    .setId(UUID.randomUUID().toString())<br>    <span>//jwt 的过期时间</span><br>    .setExpiration(expireDate)<br>    <span>// 这里是设置加密的私钥和加密类型</span><br>    .signWith(getPrivateKey(), SignatureAlgorithm.RS256)<br>    <span>//生成 jwt信息 返回的是一个字符串类型</span><br>    .compact();<br>    }
- *完整代码
<span>@Service</span><br><span>@Slf</span>4j<br><span>@Transactional</span>(rollbackFor = Exception<span>.<span>class</span>)<br><span>public</span> <span>class</span> <span>IJWTServiceIpml</span> <span>implements</span> <span>IJWTService</span> </span>{<br><br>    <span>@Autowired</span><br>    <span>private</span> EcommerceUserDao ecommerceUserDao;<br><br>    <span>@Override</span><br><br>    <span><span>public</span> String <span>generateToken</span><span>(String username, String password)</span> <span>throws</span> Exception </span>{<br>        <span>return</span> generateToken(username, password, <span>0</span>);<br>    }<br><br>    <span>@Override</span><br>    <span><span>public</span> String <span>generateToken</span><span>(String username, String password, Integer expireTime)</span> <span>throws</span> Exception </span>{<br>        <span>//首先需要验证用户是否通过授权校验,即 输入的用户名和密码能否寻找到匹配数据表的记录</span><br>        EcommerceUser ecommerceUser = ecommerceUserDao.findByUsernameAndPassword(username, password);<br>        <span>if</span> (ecommerceUser == <span>null</span>) {<br>            log.error(<span>"can not find user:[{}],[{}]"</span>, username, password);<br>            <span>return</span> <span>null</span>;<br>        }<br><br>        <span>//Token 中塞入对象, 即 JWT中 储存的对象,后端拿到这些信息 就可以知道那个用户在操作</span><br>        LoginUserinfo loginUserinfo = <span>new</span> LoginUserinfo(<br>                ecommerceUser.getId(), ecommerceUser.getUsername()<br>        );<br><br>        <span>if</span> (expireTime <= <span>0) {<br>            expireTime = AuthorCanstant.DEFAULT_EXPIRE_DAY;<br>        }<br>        <span>//计算超时时间</span><br>        ZonedDateTime zdt = LocalDate.now().plus(expireTime, ChronoUnit.DAYS)<br>                .atStartOfDay(ZoneId.systemDefault());<br>        Date expireDate = Date.from(zdt.toInstant());<br><br>        <span>return</span> Jwts.builder()<br>                <span>//这里 claim 其实就是 jwt 的 payload 对象 --> KV</span><br>                .claim(CommonCanstant.JWT_USER_INFO_KEY, JSON.toJSONString(loginUserinfo))<br>                <span>// jwt id 表示是 jwt的id</span><br>                .setId(UUID.randomUUID().toString())<br>                <span>//jwt 的过期时间</span><br>                .setExpiration(expireDate)<br>                <span>// 这里是设置加密的私钥和加密类型</span><br>                .signWith(getPrivateKey(), SignatureAlgorithm.RS256)<br>                <span>//生成 jwt信息 返回的是一个字符串类型</span><br>                .compact();<br>    }<br><br>    <span>@Override</span><br>    <span><span>public</span> String <span>registerUserAndGenerateToken</span><span>(UsernameAndPassword usernameAndPassword)</span> <span>throws</span> Exception </span>{<br>        <span>//先去校验 用户名是否存在 如果存在 不能重复注册</span><br>        EcommerceUser oldUser = ecommerceUserDao.findByUsername(usernameAndPassword.getUsername());<br>        <span>if</span> (<span>null</span> != oldUser) {<br>            log.error(<span>"username is registered:[{}]"</span>, oldUser.getUsername());<br>            <span>return</span> <span>null</span>;<br>        }<br>        EcommerceUser ecommerceUser = <span>new</span> EcommerceUser();<br>        ecommerceUser.setUsername(usernameAndPassword.getUsername());<br>        ecommerceUser.setPassword(usernameAndPassword.getPassword()); <span>//MD5 编码以后</span><br>        ecommerceUser.setExtraInfo(<span>"{}"</span>);<br><br>        <span>//注册一个新用户 写到一个 记录表中</span><br>        ecommerceUser = ecommerceUserDao.save(ecommerceUser);<br><br>        log.info(<span>"regiter user success:[{}],[{}]"</span>, ecommerceUser.getUsername());<br><br>        <span>//生成 token 并且返回</span><br>        <span>return</span> generateToken(ecommerceUser.getUsername(), ecommerceUser.getPassword());<br>    }<br><br>    <span>/*<br>     * 根据本地储存的私钥获取到 PrivateKey对象<br>     * */</span><br>    <span><span>private</span> PrivateKey <span>getPrivateKey</span><span>()</span> <span>throws</span> Exception </span>{<br><br>        <span>//使用给定的编码密钥创建一个新的PKCS8EncodedKeySpec。</span><br>        PKCS8EncodedKeySpec priPKCS8 = <span>new</span> PKCS8EncodedKeySpec(<span>new</span> BASE64Decoder().decodeBuffer(AuthorCanstant.PRIVATE_KEY));<br>        <span>// 设置生成新密钥的工厂加密方式</span><br>        KeyFactory keyFactory = KeyFactory.getInstance(<span>"RSA"</span>);<br>        <span>//返回生成好的密钥</span><br>        <span>return</span> keyFactory.generatePrivate(priPKCS8);<br>    }<br>}<br></= <span>
之后我们的授权都会使用到以上的方法
- *Controller
我们需要给注册用户和生成token 一个程序的入口
就是我们的 AuthorityController,这里可以用到我们之前使用的注解@IgnoreResponseAdvice我们为啥那么不让他封装呢,我们需要验证,单纯的 JwtToken对象就可以了,不需要封装和转化
<span>@Slf</span>4j<br><span>@RestController</span><br><span>@RequestMapping</span>(<span>"/authority"</span>)<br><span>public</span> <span><span>class</span> <span>AuthorityConroller</span> </span>{<br>    <span>private</span> <span>final</span> IJWTService ljwtService;<br><br>    <span><span>public</span> <span>AuthorityConroller</span><span>(IJWTService ljwtService)</span> </span>{<br>        <span>this</span>.ljwtService = ljwtService;<br>    }<br><br>    <span>/*<br>     * 从授权中心 获取 token (其实就是登陆功能) 且返回信息中没有统一响应的包装<br>     * */</span><br>    <span>@IgnoreResponseAdvice</span><br>    <span>@PostMapping</span>(<span>"/token"</span>)<br>    <span><span>public</span> JwtToken <span>token</span><span>(@RequestBody UsernameAndPassword usernameAndPassword)</span> <span>throws</span> Exception </span>{<br>        <span>//通常 日志里不会答打印用户的信息 防止泄露,我们这本身就是一个授权服务器,本身就不对外开放,所以我们可以打印用户信息到日志方便查看</span><br>        log.info(<span>"request to get token with param:[{}]"</span>, JSON.toJSONString(usernameAndPassword));<br>        <span>return</span> <span>new</span> JwtToken(ljwtService.generateToken(<br>                usernameAndPassword.getUsername(),<br>                usernameAndPassword.getPassword()));<br>    }<br><br>    <span>/*注册用户并且返回注册当前用户的token 就是通过授权中心常见用户*/</span><br>    <span>@IgnoreResponseAdvice</span><br>    <span>@PostMapping</span>(<span>"/register"</span>)<br>    <span><span>public</span> JwtToken <span>register</span><span>(@RequestBody UsernameAndPassword usernameAndPassword)</span> <span>throws</span> Exception </span>{<br>        log.info(<span>"register user with param:[{}]"</span>, JSON.toJSONString(usernameAndPassword));<br>        <span>return</span> <span>new</span> JwtToken(ljwtService.registerUserAndGenerateToken(usernameAndPassword));<br>    }<br>}
鉴权编码实现
这里我们打鉴权 放到公共模块里 为什么呢,这里我们不止是鉴权中心还有其他的服务也要用到鉴权服务,秉着封装的思想,我们提取公共的方法放到 Common里面
创建JWT Token解析类TokenParseUtil
<span>/**<br> * <span>@author</span> : 冷环渊<br> * <span>@date</span> : 2021/12/5<br> * <span>@context</span>: JWT Token 解析工具类<br> * <span>@params</span> :  null<br> * <span>@return</span> :  * <span>@return</span> : null<br> */</span><br><span>public</span> <span><span>class</span> <span>TokenParseUtil</span> </span>{<br><br>    <span><span>public</span> <span>static</span> LoginUserinfo <span>parseUserInfoFromToken</span><span>(String token)</span> <span>throws</span> Exception </span>{<br>        <span>if</span> (<span>null</span> == token) {<br>            <span>return</span> <span>null</span>;<br>        }<br>        Jws<claims> claimsJws = parseToken(token, getPublicKey());<br>        Claims body = claimsJws.getBody();<br>        <span>//如果 Token 已经过期返回 null</span><br>        <span>if</span> (body.getExpiration().before(Calendar.getInstance().getTime())) {<br>            <span>return</span> <span>null</span>;<br>        }<br>        <span>//     返回 Token中保存的用户信息</span><br>        <span>return</span> JSON.parseObject(<br>                body.get(CommonCanstant.JWT_USER_INFO_KEY).toString(), LoginUserinfo<span>.<span>class</span><br>        )</span>;<br>    }<br><br>    <span>/*<br>     * 通过公钥去解析 JWT Token<br>     * */</span><br>    <span><span>private</span> <span>static</span> Jws<claims> <span>parseToken</span><span>(String token, PublicKey publicKey)</span> </claims></span>{<br>        <span>// 用设置签名公钥,解析claims信息 token</span><br>        <span>return</span> Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);<br>    }<br><br>    <span>/*<br>     * 根据本地存储的公钥获取到 getPublicKey<br>     * */</span><br>    <span><span>public</span> <span>static</span> PublicKey <span>getPublicKey</span><span>()</span> <span>throws</span> Exception </span>{<br><br>        <span>//解码器 我们设置解码器 将公钥放进去</span><br>        X509EncodedKeySpec keySpec = <span>new</span> X509EncodedKeySpec(<br>                <span>new</span> BASE64Decoder().decodeBuffer(CommonCanstant.PUBLIC_KEY)<br>        );<br>        <span>//创建 RSA 实例 通过示例生成公钥对象</span><br>        <span>return</span> KeyFactory.getInstance(<span>"RSA"</span>).generatePublic(keySpec);<br>    }<br>}<br></claims>
这里是涉及到一个问题 ,token要是传输的不是jwt token对象,会跑出异常,没有兜底,
其实这里这问题其实也不成立,应为你没有传入token对象,我们这里抛出异常是正确的,也不会影响其他服务,之后搭配sentinel和豪猪哥 可以实现异常重启等等,这里我们就先不编写兜底方法,以解析jwt token为主。
验证鉴权授权
我们写一个 test 类来测试 授权和鉴权拿到对象,是否有效
<span>/**<br> * <span>@author</span> : 冷环渊<br> * <span>@date</span> : 2021/12/5<br> * <span>@context</span>: JWT 相关测试类<br> * <span>@params</span> :  null<br> * <span>@return</span> :  * <span>@return</span> : null<br> */</span><br><span>@Slf</span>4j<br><span>@SpringBootTest</span><br><span>@RunWith</span>(SpringRunner<span>.<span>class</span>)<br><span>public</span> <span>class</span> <span>JWTServiceTest</span> </span>{<br><br>    <span>@Autowired</span><br>    <span>private</span> IJWTService ijwtService;<br><br>    <span>@Test</span><br>    <span><span>public</span> <span>void</span> <span>testGenerateAndParseToken</span><span>()</span> <span>throws</span> Exception </span>{<br>        String jwtToken = ijwtService.generateToken(<br>                <span>"hyc@qq.com"</span>, <span>"e10adc3949ba59abbe56e057f20f883e"</span><br>        );<br>        log.info(<span>"jwt token is:[{}]"</span>, jwtToken);<br>        LoginUserinfo userinfo = TokenParseUtil.parseUserInfoFromToken(jwtToken);<br>        log.info(<span>"userinfo by jwt prase token :[{}]"</span>, JSON.toJSONString(userinfo));<br>    }<br>}
启动测试查看结果
eyJhbGciOiJSUzI1NiJ9<br><br>.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjExLFwidXNlcm5hbWVcIjpcImh5Y0BxcS5jb21cIn0iLCJqdGkiOiIzNDgwNjdjMi00MTBlLTQ3MjItYmM3ZS02NWQyYmNmYTRkN2MiLCJleHAiOjE2Mzg3MjAwMDB9<br><br>.ZbFl81MkIipJSULZLf4F2X2Fb0q1TwhHIMT7nyZsZVwUxXyZnK54RlzoGM_b-kMUdKO_Tab-qEeOT6Jn--FiKmbOziWXiBx3a-k5ipthMJx0Fez-X8Acty-Pg7zukNalugiLxGb5ophQoVQWRTDmv2hytGHqiV71HVyErznkJa36QQr6QsjXqlJleo3BBt-<span>6</span>BFzdTFPLUmdTEJ4XsmZBa_acUDGBhY0_tU2gYtKBWhwvMCknuyCcV-_GVI5EvgMIKRpeFSZrWfTsDG2y1MFcyzjKE6jnzek-YwT3XkzQ8eGzUbiOlaU_Zx5OJah-UtrKwqlAw9WbO71pNgEBefdsYw
这是封装好的 JWT Token 这里我们可以看到三个点分别分割 了 header和payload以及签名,和我们之前讲的 结构一模一样,
userinfo by jwt prase token :[{<span>"id"</span>:11,<span>"username"</span>:<span>"hyc@qq.com"</span>}]
获取到的我们放在jwt 里面需要传递的对象
验证对外提供的接口是否好用
这里我们编写 http脚本来测试对外题提供的接口是否有用
- *Token 方法
<span>### 获取 Token -- 登录功能实现</span><br>POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token<br>Content-Type: application/json<br><br>{<br>  <span>"username"</span>: <span>"hyc@qq.com"</span>,<br>  <span>"password"</span>: <span>"e10adc3949ba59abbe56e057f20f883e"</span><br>}
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token<br><br>HTTP/1.1 200 <br>Content-Type: application/json<br>Transfer-Encoding: chunked<br>Date: Sun, 05 Dec 2021 15:35:52 GMT<br>Keep-Alive: timeout=60<br>Connection: keep-alive<br><br>{<br>  <span>"token"</span>: <span>"eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjExLFwidXNlcm5hbWVcIjpcImh5Y0BxcS5jb21cIn0iLCJqdGkiOiIxNDU1M2FjZi1lZmE5LTQ4OTgtOTliYS1hNzA4NWI4MjU4MzAiLCJleHAiOjE2Mzg3MjAwMDB9.AlOpo6uf97R20ZLojXeun-3MK8DpSYlWxEygvDrtQeWaM9R0iKx-iW1VXnK6WoEntvqPxIrmPA7khjl3dXPa8kQHtdq-LVO7BDuZZDiQyZ64ZS7A9jWZr5JReSWBUSR1YUnsOvBRMkx4JVcAF3_W7nHwd722FFzOZRCr72hLHQIKpsugKtqjMEtaiEW0vcqphCYRJTAO_rQx1Lb1eVVg_Ufur0qSlKkV5dSJ0x3x9mc9UZRckwN0rrP7wQxZcrxJvKTfX7CkRRSO-CxZbG4WLokSaMtaGBMWU-7KGq7HSCZ0yuOgbbLdouHncsp6VD2tNLFdWSdJ_whCIbZxfX8R7w"</span><br>}
获取 token 成功
这里他没有被响应包裹,证明我们之前的选择屏蔽注解也生效了,很符合我们的预期
- *验证如果记录数据表没有是否会返回null
<span>### 获取 Token -- 登录功能实现</span><br>POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token<br>Content-Type: application/json<br><span>### 随便写的id</span><br><br>{<br><span>"username"</span>: <span>"hyc1111@qq.com"</span>,<br><span>"password"</span>: <span>"e10adc3949ba59abbe56e057f20f883e"</span><br>}
返回结果 也符合我们预期 是 null
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token<br><br>HTTP/1.1 200 <br>Content-Type: application/json<br>Transfer-Encoding: chunked<br>Date: Sun, 05 Dec 2021 15:40:44 GMT<br>Keep-Alive: timeout=60<br>Connection: keep-alive<br><br>{<br>  <span>"token"</span>: null<br>}
- *register
<span>### 注册用户并返回 Token -- 注册功能实现</span><br>POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register<br>Content-Type: application/json<br><br>{<br>  <span>"username"</span>: <span>"hyc@qq.com"</span>,<br>  <span>"password"</span>: <span>"e10adc3949ba59abbe56e057f20f883e"</span><br>}
这个用户之前是注册过的,我们来看一下是否会返回我们预期的处理
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register<br><br>HTTP/1.1 200 <br>Content-Type: application/json<br>Transfer-Encoding: chunked<br>Date: Sun, 05 Dec 2021 15:42:00 GMT<br>Keep-Alive: timeout=60<br>Connection: keep-alive<br><br>{<br>  <span>"token"</span>: null<br>}
- *现在我们去注册一个新的用户
<span>### 注册用户并返回 Token -- 注册功能实现</span><br>POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register<br>Content-Type: application/json<br><br>{<br>  <span>"username"</span>: <span>"hyc11@qq.com"</span>,<br>  <span>"password"</span>: <span>"e10adc3949ba59abbe56e057f20f883e"</span><br>}
符合预期结果,创建了我们预期的对象,这个时候我们去看一下数据表
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register<br><br>HTTP/1.1 200 <br>Content-Type: application/json<br>Transfer-Encoding: chunked<br>Date: Sun, 05 Dec 2021 15:42:57 GMT<br>Keep-Alive: timeout=60<br>Connection: keep-alive<br><br>{<br>  <span>"token"</span>: <span>"eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEyLFwidXNlcm5hbWVcIjpcImh5YzExQHFxLmNvbVwifSIsImp0aSI6IjMxNDc0NmIwLTMyOGYtNDZkNS05ZTIwLTg3YjI0OWY1ZjZkOCIsImV4cCI6MTYzODcyMDAwMH0.MKxk-Q4BG5kaYFAsLiy13trtk_gDFmCKORpdE4EAwgSVecXFQcYfT1VvqSAKvoQLFsSlQAxOR5elV8CFOoKwAomwqdyyghZp63NKJ2smRbg3Y-4jWBzFVsUgcjOY2fwh7oNTdHEsWmLBYAh5r0hm_MysZsUEsE-cwb3sw8NSMk1OZp0J6tcRras7V1Uw5xXH8OnCoq2cUfdynJMHS29EzJT1TFPb8unVQ_A1RWodsHdK3n1Bl4wFbJjMtnHx7vzOeAUSNJx1XpAGdo0xYHK6HBpS9E1KBS3x1AnYFONM0DKd4-_QxMkBW1kkg2uWrRpf3GYZF20FKxXgmBAPHGZhew"</span><br>}
对象生成,功能验证一切正常
鉴权服务中心总结
对比基于Token与基于服务器的身份认证
传统:
- 最为传统的做法,客户端储存 cookie 一般是 Session id 服务器存储 Session
- Session 是每次用户认证通过以后 ,服务器需要创建一条记录保存用户信息,通常是在内存中(也可以放在redis中),随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大
- 不同域名之前切换的时候,请求可能会被禁止,即跨越问题
基于token:
- JWT与Session的差异相同点是,他们都是存储用户信息。然而Session是在服务器端的,而JWT是在客户端的
- JWT方式将用户状态分散到了客户端中,可以明显减轻请服务器的内存压力,服务端只需要用算法解析客户端的token就可以得到信息
- 两者优缺点的对比
- 解析方法:JWT使用算法直接解析得到用户信息;Session需要额外的数据映射。实现匹配
- 管理方法:JWT只有过期时间的限制,Session 数据保存在服务器,可控性更强
- 跨平台:JWT就是一段字符串,可以任意传播,Session跨平台需要有统一的解析平台,较为繁琐
- 时效性:JWT一旦生成 独立存在,很难做到特殊的控制;Session时效性完全由服务端的逻辑说了算
TIPS :各自都有优缺点,都是登陆和授权的解决方案
来源:blog.csdn.net/doomwatcher/article/details/121743887
ps:如果您觉文章有用,动动小手点个在看,点个赞再走吧!
Original: https://www.cnblogs.com/konglxblog/p/16749347.html
Author: china_coding
Title: 搭建SpringCloud Alibaba鉴权中心服务(详细教程)
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/570365/
转载文章受原作者版权保护。转载请注明原作者出处!