一、名词解析
**UAA:**用户通过接入方(应用)登录,接入方采取OAuth2.0方式在统一认证服务(UAA)中认证。
认证授权:session、Spring Security、Shiro。
1、定义
2、授权的数据模型(5、6张表)
二、基于session的认证方式
- 指定Root Context的配置类
- 指定servletContext的配置类
- urlMapping
1、目录结构:
2、Session常用方法:
方法 | 含义 |
---|
HttpSession getSession(Boolean create) | 获取当前HttpSession对象 | void setAttribute(String name,Object value) | 向session中存放对象 | object getAttribute(String name) | 从session中获取对象 | void removeAttribute(String name); | 移除session中对象 | void invalidate() | 使HttpSession失效 | 略… | |
3、代码:
登录
@RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
return userDto.getUsername() +"登录成功";
}
退出:
@GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
public String logout(HttpSession session){
session.invalidate();
return "退出成功";
}
增加Spring MVC连接器
三、Spring Security快速上手
1、目录结构
类名 | 解释 | | | 依赖关系 |
---|
ApplicationConfig | Spring容器配置 | 配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。 | 初始化 | | WebConfig | Servlet Context配置(同security-springmvc) | 1.视频解析器 2.控制器 | 初始化 | implements WebMvcConfigurer | SpringApplicationInitializer | 加载Spring容器 | 1.指定位置(WebSecurityConfig) 2.指定位置(WebConfig) 3.指定url-mapping | 初始化 | extends AbstractAnnotationConfigDispatcherServletInitializer | WebSecurityConfig | 安全配置(提供账号/密码)() | 1.定义用户信息服务(查询用户信息) 2.密码编码器 3.安全拦截机制(最重要) | | extends WebSecurityConfigurerAdapter | SpringSecurityApplicationInitializer | SringSecurity 初始化 | | | extends AbstractSecurityWebApplicationInitializer | LoginController | 登录接口 | | | |
2、WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("redirect:/login");
}
}
3、 WebSecurityConfig配置解析
安全配置
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
密码编译器:
NoOpPasswordEncoder:不采用任何编码(简单字符串比较)
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.successForwardUrl("/login-success");
}
4、代码源包
security-spring-security.zip
三、Spring boot集成Spring Security
1、代码资源包
security-spring-boot.zip
2、目录结构
pom.xml
application.properties
StartUp.java
config-WebConfig:
config-WebSecurityConfig:
controller-LoginController:
四、工作原理
1、结构图
? Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
? 当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类。
SpringSecurityFilterChain:
- 主要的拦截器。
- 是多个Filter的集合。
- 是个代理:代理用户请求去调用各个Filter。
Authentication Manager
AccessDecision Manager
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时 这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认 证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的UML图示。
下面介绍过滤器链中主要的几个过滤器及其作用:
SecurityContextPersistenceFilter(入口/出口)
这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给SecurityContextHolder。在请求完成后将SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
UsernamePasswordAuthenticationFilter(认证)
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和AuthenticationFailureHandler,这些都可以根据需求做相关改变;
FilterSecurityInterceptor(授权)
是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
ExceptionTranslationFilter(异常处理)
能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
2、认证流程
让我们仔细分析认证过程:
- 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
- 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
- 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
- SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个
List<AuthenticationProvider> 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。
解析:
- 最主要的是
UsernamePasswordAuthenticationFilter 和DaoAuthenticationProvider 这两个。 UsernamePasswordAuthenticationFilter 是最外层的认证管理器。DaoAuthenticationProvider 是具体干活的,负责查询与校验。AuthenticationManager 负责选择适合的密码校验管理器。
3、认证管理器(AuthenticationManager)
UserDetailsService和DaoAuthenticationProvider关系
认证管理器(AuthenticationManager)委托 AuthenticationProvider完成认证工作。
AuthenticationProvider是一个接口:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> var1);
}
Spring Security中维护着一个 List 列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProvider1,短信登录时使用AuthenticationProvider2等等.
每个AuthenticationProvider需要实现**supports()**方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken,它是一个Authentication,里面封装着用户提交的用户名、密码信息。
在DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider发现以下代码:
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
也就是说当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理。
4、(根据用户名提取用户信息)UserDetailsService
对比DaoAuthenticationProvider 和UserDetailsService 区别:
? 很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认证流程,同时会把UserDetails填充至Authentication。
通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
Spring Security提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。
自定义UserDetailsService :
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("username="+username);
UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
return userDetails;
}
}
5、密码比较方式(PasswordEncoder )
DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求Authentication中的密码做对比呢?
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如下声明即可,如下:
@Bean public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
-
用户输入密码(明文 ) -
DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码) -
DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。
NoOpPasswordEncoder的校验规则拿 输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则校验失败。
实际项目中推荐使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等。
6、授权流程
通过快速上手我们知道,Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。
析授权流程:
-
拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子类拦截。 -
获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection 。 SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读 取访问策略如:
http.authorizeRequests()
.antMatchers("/r/r1")
.hasAuthority("p1")
.antMatchers("/r/r2")
.hasAuthority("p2")
...
- 最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
三、分布式验证
1、流程图
流程描述:
-
(1)用户通过接入方(应用)登录,接入方采取OAuth2.0方式在统一认证服务(UAA)中认证。 -
(2)认证服务(UAA)调用验证该用户的身份是否合法,并获取用户权限信息。 -
(3)认证服务(UAA)获取接入方权限信息,并验证接入方是否合法。 -
(4)若登录用户以及接入方都合法,认证服务生成jwt令牌返回给接入方,其中jwt中包含了用户权限及接入方权限。 -
(5)后续,接入方携带jwt令牌对API网关内的微服务资源进行访问。 -
(6)API网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。 -
(7)如果接入方的权限没问题,API网关将原请求header中附加解析后的明文Token,并将请求转发至微服务。 -
(8)微服务收到请求,明文token中包含登录用户的身份和权限信息。因此后续微服务自己可以干两件事:1,用户授权拦截(看当前用户是否有权访问该资源)2,将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
四、OAuth2.0认证
1、认证流程
五、JWT 实现微服务鉴权
5.1 什么是微服务鉴权
我们之前已经搭建过了网关,使用网关在网关系统中比较适合进行权限校验。
那么我们可以采用JWT的方式来实现鉴权校验。
5.2 JWT
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的?BASE64Encoder?和?BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码
载荷(playload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
(1)标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token。
(2)公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
(3)私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
定义一个payload:
{"sub":"1234567890","name":"John Doe","admin":true}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
5.3 JJWT签发与验证token
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
官方文档:
https://github.com/jwtk/jjwt
5.3.1 创建token
(1)新建项目中的pom.xml中添加依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
(2) 创建测试类,代码如下
JwtBuilder builder= Jwts.builder()
.setId("888")
.setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"itcast");
System.out.println( builder.compact() );
运行打印结果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y
再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。
5.3.2 解析token
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y";
Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
运行打印效果:
{jti=888, sub=小白, iat=1557904181}
试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.
5.3.3 设置过期时间
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。
(1)创建token 并设置过期时间
long currentTimeMillis = System.currentTimeMillis();
Date date = new Date(currentTimeMillis);
JwtBuilder builder= Jwts.builder()
.setId("888")
.setSubject("小白")
.setIssuedAt(new Date())
.setExpiration(date)
.signWith(SignatureAlgorithm.HS256,"itcast");
System.out.println( builder.compact() );
解释:
.setExpiration(date)//用于设置过期时间 ,参数为Date类型数据
运行,打印效果如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI
(2)解析TOKEN
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI";
Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
打印效果:
当前时间超过过期时间,则会报错。
5.3.4 自定义claims
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。
创建测试类,并设置测试方法:
创建token:
@Test
public void createJWT(){
long currentTimeMillis = System.currentTimeMillis();
currentTimeMillis+=1000000L;
Date date = new Date(currentTimeMillis);
JwtBuilder builder= Jwts.builder()
.setId("888")
.setSubject("小白")
.setIssuedAt(new Date())
.setExpiration(date)
.claim("roles","admin")
.signWith(SignatureAlgorithm.HS256,"itcast");
System.out.println( builder.compact() );
}
运行打印效果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI
解析TOKEN:
//解析
@Test
public void parseJWT(){
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI";
Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
}
运行效果:
5.4 畅购微服务鉴权代码实现
5.4.1 思路分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FS4X5tl2-1651829933781)(/Users/mlamp/Downloads/01项目讲义(1)]/day03/images/1557906391792.png)
1. 用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进行登录
2. 用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户
3. 用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN
4. 网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误
5.4.2 系统微服务签发token
(1)在changgou_service_system中创建类: JwtUtil
package com.changgou.system.util;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
public class JwtUtil {
public static final Long JWT_TTL = 3600000L;
public static final String JWT_KEY = "itcast";
public static String createJWT(String id, String subject, Long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id)
.setSubject(subject)
.setIssuer("admin")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
return builder.compact();
}
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
}
(2)修改AdminController的login方法, 用户登录成功 则 签发TOKEN
@PostMapping("/login")
public Result login(@RequestBody Admin admin){
boolean login = adminService.login(admin);
if(login){
Map<String,String> info = new HashMap<>();
info.put("username",admin.getLoginName());
String token = JwtUtil.createJWT(UUID.randomUUID().toString(), admin.getLoginName(), null);
info.put("token",token);
return new Result(true, StatusCode.OK,"登录成功",info);
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错误");
}
}
使用postman 测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qw0Tku2J-1651829933781)(/Users/mlamp/Downloads/01项目讲义(1)]/day03/images/4_3.png)
5.4.3 网关过滤器验证token
(1)在changgou_gateway_system网关系统添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
(2)创建JWTUtil类
package com.changgou.gateway.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class JwtUtil {
public static final Long JWT_TTL = 3600000L;
public static final String JWT_KEY = "itcast";
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
(3)创建过滤器,用于token验证
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
private static final String AUTHORIZE_TOKEN = "token";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (request.getURI().getPath().contains("/admin/login")) {
return chain.filter(exchange);
}
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(AUTHORIZE_TOKEN);
if (StringUtils.isEmpty(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
try {
JwtUtil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
(4)测试:
注意: 数据库中管理员账户为 : admin , 密码为 : 123456
如果不携带token直接访问,则返回401错误
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cpxIhZGU-1651829933781)(/Users/mlamp/Downloads/01项目讲义(1)]/day03/images/4-5.png)
如果携带正确的token,则返回查询结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h43nnlw8-1651829933782)(/Users/mlamp/Downloads/01项目讲义(1)]/day03/images/4-4.png)
|