Spring_Security+JWT+oauth2.0

SpirngSecurity 入门案例

2.1、入门案例

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

引入sercurity依赖之后,当访问后台资源,security都会判断你是否登录,没有登录就跳到登陆页面。(除了你放行的请求)
账户密码是security默认给的,user , 密码是随机生成的。

2.2、Spring Security 基本原理

SpringSecurity 本质是一个过滤器链:
从启动是可以获取到过滤器链:遇到相关请求就会执行相关拦截判断

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter 
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter 
org.springframework.security.web.session.SessionManagementFilter 
org.springframework.security.web.access.ExceptionTranslationFilter 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

代码底层流程:重点看三个过滤器:
FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。
image.png
super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。 ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
image.png
UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码。
image.png

2.3、两个重要接口

1、UserDetailsService 接口

实际开发中,从数据库中查找用户的方法是在 实现 这个接口的类中声明的
image.png
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

返回值为UserDetails,是一个接口
这个接口中定义了一些方法

// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();

image.png
以后我们只需要使用 User 这个实体类即可!该类实现了UserDetails接口,用它来接受从数据库中查的用户信息
image.png

2、PasswordEncoder 接口

Spring Security有要求,我们在自定义登录逻辑时,容器中必须要有PasswordEncoder实例,所以我们需要编写配置文件,将其放入容器中。

// 把传过来的原始密码进行加密
String encode(CharSequence rawPassword);

// 验证用户输入的密码和数据库中的密码是否匹配。如果密码匹配,则返回 true;
如果不匹配,则返回 false。第一个参数表示需要被加密的密码。
第二个参数已经加密过存储在数据库中的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);

// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
false。默认返回 falsedefault boolean upgradeEncoding(String encodedPassword) {
return false; }

PasswordEncoder接口含有多个实现类,BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时经常使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10.

@Test
public void test01(){
    // 创建密码解析器
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    // 对密码进行加密
    String srgstart = bCryptPasswordEncoder.encode("srgstart");
    // 打印加密之后的数据
    System.out.println("加密之后数据:\t"+srgstart);
    //判断原字符加密后和加密之前是否匹配
    boolean result = bCryptPasswordEncoder.matches("srgstart", srgstart);
    // 打印比较结果
    System.out.println("比较结果:\t"+result);
}

2.4、登录的相关操作和访问权限的设置

1、将PasswordEncoder注入到容器中
2、实现UserDetailService接口,编写自定义登录逻辑(使用数据库数据代替自动给的用户名和密码)

/**
 * @author srgstart
 * @create 2024/01/22 14:46
 * @description 自定义登录逻辑
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1、查询数据库判断用户名是否存在,如果不存在就会抛出UsernameNotFoundException异常
        if (!"admin".equals(username)) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        // 2、把查询出来的密码(注册时已经加密过)进行解析,或者直接把密码放入构造方法
        String encodePassword = passwordEncoder.encode("123");
        return new User(username, encodePassword, AuthorityUtils.
                commaSeparatedStringToAuthorityList("admin,normal"));
    }
}

3、继承 WebSecurityConfigurerAdapter 重写configure方法 是为了我们自己修改默认配置,可以修改登录页,权限,角色等

/**
 * @author srgstart
 * @create 2024/01/22 14:40
 * @description Spring Security配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //表单提交
        http.formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                // 发现/login时认为是登录,必须和表单提交的地址一样,去执UserDetailsServiceImpl
                .loginProcessingUrl("/login")
                //自定义登录页面
                .loginPage("/login.html")
//                // 登录成功后跳转的页面(必须是post请求接口)
//                .successForwardUrl("/index")
                // 登录成功处理器(适用于前后端分离)
                .successHandler(new LocalAuthenticationSuccessHandler())
//                // 登录失败后跳转的页面(必须是post请求接口)
//                .failureForwardUrl("/error")
                // 登录失败处理器(适用于前后端分离)
                .failureHandler(new LocalAuthenticationFailureHandler());

        //授权认证
        http.authorizeRequests()
                //login.htmL不需要被认证
                .antMatchers("/login.html", "/error.html").permitAll()
                //所有请求都必须被认证,必须登录之后被访问
                .anyRequest().authenticated();

        // 关闭csrf维护
        http.csrf().disable();
    }
}

4、前后端分离状态下,如何将登录信息返回给前端(自定义登录状态处理器)
Spring Security自定义登录验证及登录返回结果_springsecurity登录返回登录数据-CSDN博客
anyRequest、antMatchers

2.5、角色权限判断

1、hasAuthority(String)

弊端就是hasAuthority() 括号中的权限(可以有多个权限)必须都要满足,才能访问,都要满足

判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建User对象时指定的。下图中admin和normal就是用户的权限。admin和normal。严格区分大小写。

image.png
在配置类中通过hasAuthority('admin")设置具有admin权限时才能访问。

antMatchers("/main1.html").hasAuthority("admin")

2、hasAnyAuthority(String…)

只要满足其中一种权限就可以访问

如果用户具备给定权限中某一个,就允许访问。严格区分大小写。

antMatchers("/main1.html").hasAnyAuthority("admin","normal")

3、hasRole方法

如果用户具备给定角色就允许访问,否则出现 403。
如果当前主体具有指定的角色,则返回 true。

  • 底层源码:

image.png

  • 给用户添加角色

image.png

  • 修改配置文件:

注意配置文件中不需要添加”ROLE_“,因为上述的底层代码会自动添加与之进行匹配。
image.png

4、hasAnyRole方法

表示用户具备任何一个条件都可以访问。

  • 给用户添加角色:

image.png

  • 修改配置文件:

image.png

2.6、IP地址判断

修改配置文件:
image.png

2.7、基于注解的访问控制

1、在Spring Security中提供了一些访问控制的注解。这些注解都是默认不可用的,需要通过 @EnableGlobalMethodSecurity进行开启后使用。如果设置的条件允许,程序正常执行。如果不允许会报500(org.springframework.security.access.AccessDeniedException:不允许访问)
2、这些注解可以写到Service接口或方法上上,也可以写到Controller或Controller的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。

1、@Secured

只能对角色进行判断
添加在方法上,判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。
满足其中一个角色就行。满足就可以访问这个方法
在启动类开启注解:
@EnableGlobalMethodSecurity(securedEnabled = true)

@Secured({"ROLE_admin","ROLE_sales"})
@GetMapping("/user")
public String user(){
    return "user";
}

2、@PreAuthorize@PostAuthorize

  • @PreAuthorize表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
  • @PostAuthorize表示方法或类执行结束后判断权限,此注解很少被使用到。

先开启注解功能:
@EnableGlobalMethodSecurity(prePostEnabled = true)

@RequestMapping("/preAuthorize")
@ResponseBody
//@PreAuthorize("hasRole('ROLE_admin')")
@PreAuthorize("hasAnyAuthority('role')")
public String preAuthorize(){
    System.out.println("preAuthorize");
    return "preAuthorize"; 
}

2.8、RememberMe功能

Spring Security中Remember Me为"记住我"功能,用户只需要在登录时添加remember-me复选框,取值为true。Spring Security会自动把用户信息存储到数据源中,以后就可以不登录进行访问

1、添加依赖
Spring Security实现Remember Me功能时底层实现依赖Spring-JDBC,所以需要导入Spring-JDBC。以后多使用MyBatis框架而很少直接导入spring-jdbc,所以此处导入mybatis启动器同时还需要添加MySQL驱动

<!-mybatis依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>
<!-mysq1数据库依赖-->
<dependency>
    <groupId>mysq1</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.18</version>
</dependency>

2、配置数据源(yaml or properties中)
3、编写配置文件
在SecurityConfig中配置

// 注入数据源
@Autowired
private DataSource dataSource;

/**
 * 配置操作数据库的对象 JdbcTokenRepositoryImpl
 *    通常使用JdbcTokenRepositoryImpl的父类,也就是 PersistentTokenRepository来创建对象
 */
@Bean
public PersistentTokenRepository persistentTokenRepository(){
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    // 设置要操作的是哪个数据源
    jdbcTokenRepository.setDataSource(dataSource);
    // 自动创建表,第一次启动项目会自动创建,以后重新启动项目就要注释掉!
    jdbcTokenRepository.setCreateTableOnStartup(true);
    return jdbcTokenRepository;
}
http.rememberMe().tokenRepository(persistentTokenRepository()) // 指定是哪个操作数据库的对象
                .tokenValiditySeconds(60*60*24*7)  // 设置有效时间,以秒为单位,默认是两周时间
                .userDetailsService(userDetailService); // 自定义登录逻辑

此处:name 属性值必须位 remember-me.不能改为其他值,除非你配置参数名称

记住我:<input type="checkbox"name="remember-me"title="记住密码"/><br/>

2.9、注销功能

// 开启自动配置的注销功能
//   http.logout();  // 开启之后,访问 /logout 就可以实现注销功能, 并且,注销成功之后,会跳到登录页。
http.logout().logoutSuccessUrl("/"); // 设置注销成功之后,跳转的页面

2.10、CSRF 理解

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-clickattack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。 从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,
Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

Spring Securityl中的CSRF
从Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。

Oauth2协议

OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。“客户端"不能直接登录"服务提供商”,只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。
OAuth 2.0 的一个简单解释 - 阮一峰的网络日志
理解OAuth 2.0 - 阮一峰的网络日志

运行流程


(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。

不难看出来,上面六个步骤之中,B是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源。
下面一一讲解客户端获取授权的四种模式。

授权模式

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。

它的步骤如下:
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

下面是上面这些步骤所需要的参数。
A步骤中,客户端申请认证的URI,包含以下参数:

  • response_type:表示授权类型,必选项,此处的值固定为"code"
  • client_id:表示客户端的ID,必选项
  • redirect_uri:表示重定向URI,可选项
  • scope:表示申请的权限范围,可选项
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

下面是一个例子。

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
        &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

C步骤中,服务器回应客户端的URI,包含以下参数:

  • code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。

下面是一个例子。

HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
          &state=xyz

D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:

  • grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
  • code:表示上一步获得的授权码,必选项。
  • redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
  • client_id:表示客户端ID,必选项。

下面是一个例子。


POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

E步骤中,认证服务器发送的HTTP回复,包含以下参数:

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

下面是一个例子。


 HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 Cache-Control: no-store
 Pragma: no-cache

 {
   "access_token":"2YotnFZFEjr1zCsicMWpAA",
   "token_type":"example",
   "expires_in":3600,
   "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
   "example_parameter":"example_value"
 }

从上面代码可以看到,相关参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存。

简化模式

密码模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

它的步骤如下:
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
B步骤中,客户端发出的HTTP请求,包含以下参数:

  • grant_type:表示授权类型,此处的值固定为"password",必选项。
  • username:表示用户名,必选项。
  • password:表示用户的密码,必选项。
  • scope:表示权限范围,可选项。

下面是一个例子。

 POST /token HTTP/1.1
 Host: server.example.com
 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
 Content-Type: application/x-www-form-urlencoded

 grant_type=password&username=johndoe&password=A3ddj3w

C步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。

 HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 Cache-Control: no-store
 Pragma: no-cache

 {
   "access_token":"2YotnFZFEjr1zCsicMWpAA",
   "token_type":"example",
   "expires_in":3600,
   "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
   "example_parameter":"example_value"
 }

整个过程中,客户端不得保存用户的密码。

客户端模式

更新令牌

如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用"更新令牌"申请一个新的访问令牌。
客户端发出更新令牌的HTTP请求,包含以下参数:

  • grant_type:表示使用的授权模式,此处的值固定为"refresh_token",必选项。
  • refresh_token:表示早前收到的更新令牌,必选项。
  • scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。

下面是一个例子。

 POST /token HTTP/1.1
 Host: server.example.com
 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
 Content-Type: application/x-www-form-urlencoded

 grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA

SpringSecurityOauth2

image.png
流程

  1. 用户访问,此时没有Token。Oauth.2 RestTemplate:会报错,这个报错信息会被Oauth2 ClientContextFilter捕获并重定向到认证服务器
  2. 认证服务器通过Authorization Endpoint进行授权,并通过AuthorizationServerTokenServices生成授权码并返回给客户端
  3. 客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生成Token并返回给客户端
  4. 客户端拿到Token去资源服务器访问资源,一般会通过Oauth2 AuthenticationManager调用ResourceServerTokenServices进行校验。校验通过可以获取资源。

授权码模式演示

UserDetailServiceImpl配置类 不变,在SecurityConfig中把 oauth/** 放行

编写授权服务器配置
client-id 和secret是用于登录到授权服务器中

package com.srgstart.springsecurityoauth2demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

/**
 * @author srgstart
 * @create 2024/02/27 19:40
 * @description 授权服务器配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 这里为了演示,把client-id 和secret存在内存中,正常情况下是从授权服务器中注册出来的
        clients.inMemory()
                .withClient("admin")
                .secret(passwordEncoder.encode("112233"))
                // 配置令牌的有效期(秒)
//                .accessTokenValiditySeconds(3600)
                // 配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                // 配置申请的权限范围
                .scopes("all")
                // 配置授权模式:授权码模式
                .authorizedGrantTypes("authorization_code");
    }
}

资源服务器配置

package com.srgstart.springsecurityoauth2demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

/**
 * @author srgstart
 * @create 2024/02/27 20:05
 * @description 资源服务器配置
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .requestMatchers()
                .antMatchers("/user/**");
    }
}

controller 资源接口

@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 获取当前用户
     * @param authentication
     * @return
     */
    @GetMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication) {
        return authentication.getPrincipal();
    }
}

测试:

  • 获取授权码

http://localhost:8080/oauth/authorize?response_type=code&client_id=admin&redirect_uri=http://www.baidu.com&scope=all

  • 获取token(在postman中记录)
  • 获取资源(在postman中记录)

密码模式演示

Redis存储Token

之前的代码我们将token 直接存在内存中,这在生产环境中是不合理的,下面我们将其改造成存储在Redis中

添加依赖和配置(yaml)

<!-- redis依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2对象池依赖-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

编写Redis配置类

package com.srgstart.springsecurityoauth2demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
 * @author srgstart
 * @create 2024/02/28 9:59
 * @description Redis配置
 */
@Configuration
public class RedisConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * 配置完之后,它会自动的将token放到redis中
     * @return TokenStore对象
     */
    @Bean
    public TokenStore redisTokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }
}

在认证服务器配置中重写下方配置,把tokenStore添加进去
image.png

JWT

常见的认证机制

1、HTTP Basic Auth
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful AP时,尽量避免采用HTTP BasicAuth
2、Cookie Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookiex对象来与服务器端的sessioni对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie的expire time使cookie在一定时间内有效。
3、OAuth
OAuth(开放授权,Open Authorization)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。如网站通过微信、微博登录等,主要用于第三方登录。
OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
下面是OAuth2的流程:
image.png
这种基于OAuth的以证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。

4、Token Auth
使用基于Token的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
1.客户端使用用户名跟密码请求登录
2.服务端收到请求,去验证用户名与密码
3.验证成功后,服务端会签发一个Token,再把这个Token发送给客户端
4.客户端收到Token以后可以把它存储起来,比如放在Cookie里
5.客户端每次向服务端请求资源的时候需要带着服务端签发的Token
6.服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据

比第一种方式更安全,比第二种方式更节约服务器资源,比第三种方式更加轻量。
具体,Token Auth的优点(Token机制相对于Cookie机制又有什么好处呢?):

  1. 支持跨域访问:Cookie,是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输,
  2. 无状态(也称:服务端可扩展行):Token:机制在服务端不需要存储session信息,因为Token自身包含了所有登用户的信息,只需要在客户端的cookie或本地介质存储状态信息
  3. 更适用CDN:可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服端只要提供API即可.
  4. 去耦:不需要绑定到一个特定的身份验证方案。Tokn可以在任何地方生成,只要在你的AP被调用的时候,可以进行Tokens生成调用即可
  5. 更适用于移动应用:当你的客户端是一个原生平台(iOS,Android,Windows10等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  6. CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
  7. 性能:一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多
  8. 不需要为登录页面做特殊处理:如果你使用Protractor做功能测试的时候,不再需要为登录页面做特殊处理
  9. 基于标准化:你的API可以采用标准化的SON Web Token(UWT).这个标准已经存在多个后端库(.NET,Ruby,Java,Python,PHP)和多家公司的支持(如:Firebase,Google,Microsoft)

JWT简介

  1. 什么是JWT

JSON Web Token (JWT)是一个开放的行业标准(RFC7S19),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被算改。
官网:https://jwt.io/
JWT令牌的优点:

  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展。
  3. 通过非对称加密算法及数字签名技术,JWT防止算改,安全性高。
  4. 资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:
JWT令牌较长,占存储空间比较大。

  1. JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部 (Header)
头部用于描述关于该JWT的最基本的信息,例如其类型(即JWT)以及签名所用的算法(如HMAC SHA256或RSA)等。这也可以被表示成一个JSON对象。

{
"alg":"Hs256",
"typ":"JWT"
}
  • typ: 是类型。
  • alg: 签名的算法,这里使用的算法是HS256算法

我们对头部的json字符串进行BASE64编码(网上有很多在线编码的网站),编码后的字符串如下:

eyJhbGcioiJIUzI1NiIsInR5cCI6IkpXVCj9

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK中提供了非常方便的BASE64Encoder和BASE64Decoder,用它们可以非常方便的完成基于BASE64的编码和解码。

负载(payload)
第二部分是负载,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

  • 标准中注册的声明(建议但不强制使用)
iss:jwt鉴发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp:jwt的过期时间,这个过期时间必须要大于签发时间
nbf:定义在什么时间之前,该jwt都是不可用的.
iat:jwt的签发时间
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
  • 公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息但不建议添加敏感信息,因为该部分在客户端可解密

  • 私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为bS64是对称解密的,意味着该部分信息可以归类为明文信息。

这个指的就是自定义的claim。比如下面那个举例中的name都属于自定的claim。这些claim跟WT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到WT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。

{
"sub":"1234567890",
"name":"John Doe",
"iat":1516239022
}

其中sub是标准的声明,name是自定义的声明(公共的或私有的)
然后将其进行base64编码,得到jwt的第二部分:

eyJzdWIioiIxMjMONTY30DkwIiwibmFtZSI6IkphbWVzIiwiYWRtaw4ionRydwV9

提示:声明中不要放一些敏感信息。

签证、签名(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  1. header(base64后的)
  2. payload(base64后的)
  3. secret(盐,一定要保密)

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:

8HI-LodOncfVDnbKIP]JqLH998duF9DSDGkx3gRPNVI

将这三部分用连接成一个完整的字符串,构成了最终的jwt:

eyJhbGcioiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIioiIxMjMONTY30DkwIiwibmFtZSI6IkpvaG4gRG91IiwiaWFOIjoxNTE2MjM5MDIyfQ.8HI-LodOncfVDnbKIP]]qLH998duF9DSDGkx3gRPNVI

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了。

JJWT使用

创建token、解析token、token过期校验
引入jjwt依赖

<!-- 整合jwt,使用jwt的token代替oauth的token(实现accessToken和JwtToken的转换)-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

测试

package com.srgstart.jjwtdemo;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.Base64Codec;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Date;

@SpringBootTest
class JjwtdemoApplicationTests {

    @Test
    void testCreateToken() {
        // 创建 JwtBuilder对象
        JwtBuilder jwtBuilder = Jwts.builder()
                // 声明的标识{"jti","2222"}
                .setId("2222")
                // 主体,用户{"sub","song"}
                .setSubject("song")
                // 创建日期{"ita","xxxxx"}
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256, "xxxxxxx");
        // 获取jwt的token
        String token = jwtBuilder.compact();
        System.out.println(token);

        System.out.println("=====================");
        String[] split = token.split("\\.");
        System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
        System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
        System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
    }

    @Test
    void parseToken() {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyMjIyIiwic3ViIjoic29uZyIsImlhdCI6MTcwOTEwODY2Mn0.wtpBovqNd-2ZxVH-6UJ9Ni0AtoxbQbuOWNm8d-qR1lY";
        // 解析token获取负载中声明的对象
        Claims claims = Jwts.parser()
                .setSigningKey("xxxxxxx")
                .parseClaimsJws(token)
                .getBody();
        System.out.println(claims.getId());
        System.out.println(claims.getSubject());
        System.out.println(claims.getIssuedAt());
    }

    @Test
    void testCreateTokenHasExp() {
        // 当前系统时间
        long now = System.currentTimeMillis();
        // 过期时间:1min
        long expTime = now + 60 * 1000;
        // 创建 JwtBuilder对象
        JwtBuilder jwtBuilder = Jwts.builder()
                // 声明的标识{"jti","2222"}
                .setId("2222")
                // 主体,用户{"sub","song"}
                .setSubject("song")
                // 创建日期{"ita","xxxxx"}
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256, "xxxxxxx")
                // 设置过期时间
                .setExpiration(new Date(expTime));
        // 获取jwt的token
        String token = jwtBuilder.compact();
        System.out.println(token);

        System.out.println("=====================");
        String[] split = token.split("\\.");
        System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
        System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
        System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
    }

    @Test
    void parseTokenHasExp() {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyMjIyIiwic3ViIjoic29uZyIsImlhdCI6MTcwOTI4MTQ3OCwiZXhwIjoxNzA5MjgxNTM4fQ.KnFO01tTLdEarS7wiVe7QsWY6NC3yn81GVwTnLav0kQ";
        // 解析token获取负载中声明的对象
        Claims claims = Jwts.parser()
                .setSigningKey("xxxxxxx")
                .parseClaimsJws(token)
                .getBody();
        System.out.println(claims.getId());
        System.out.println(claims.getSubject());
        System.out.println("签发时间: "+ claims.getIssuedAt());
        System.out.println("过期时间: "+ claims.getExpiration());
        System.out.println("当前时间: "+ new Date());
    }

}

自定义声明

SpringSecurityOauth2整合JWT

快速使用

引入jjwt依赖

<!-- 整合jwt,使用jwt的token代替oauth的token(实现accessToken和JwtToken的转换)-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

JwtTokenStoreConfig配置类

package com.srgstart.springsecurityoauth2demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * @author srgstart
 * @create 2024/03/02 10:02
 * @description JwtToken配置类
 */
@Configuration
public class JwtTokenStoreConfig {

    /**
     * 签名的key,由开发者约定
     */
    private static final String SINGING_KEY = "lst";

    /**
     * JWT令牌存储方案
     */
    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * Jwt令牌转换器
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SINGING_KEY);
        return converter;
    }
}

授权服务器配置

package com.srgstart.springsecurityoauth2demo.config;

import com.srgstart.springsecurityoauth2demo.service.UserDetailServiceImpl;
import io.jsonwebtoken.Jwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

/**
 * @author srgstart
 * @create 2024/02/27 19:40
 * @description 授权服务器配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserDetailServiceImpl userDetailService;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    @Qualifier(value = "jwtTokenStore")
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 这里为了演示,把client-id 和secret存在内存中,正常情况下是从授权服务器中注册出来的
        clients.inMemory()
                .withClient("admin")
                .secret(passwordEncoder.encode("112233"))
                // 配置令牌的有效期(秒)
//                .accessTokenValiditySeconds(3600)
                // 配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                // 配置申请的权限范围
                .scopes("all")
                // 配置授权模式:授权码模式
                .authorizedGrantTypes("authorization_code");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                //认证管理器
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailService)
                // 配置存储令牌策略
                .tokenStore(tokenStore)
                // 实现accessToken和JwtToken的转换
                .accessTokenConverter(accessTokenConverter);
    }
}

image.png

扩充Jwt中存储的内容

编写内容增强器类(不需要注入到容器中,之后会在jwtTokenStoreConfig中配置)

package com.srgstart.springsecurityoauth2demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;

import java.util.HashMap;
import java.util.Map;

/**
 * @author srgstart
 * @create 2024/03/02 10:32
 * @description Jwt内容增强器
 */
public class JwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("enhancer", "enhancer info");
        ((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}

注入到容器中

package com.srgstart.springsecurityoauth2demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * @author srgstart
 * @create 2024/03/02 10:02
 * @description JwtToken配置类
 */
@Configuration
public class JwtTokenStoreConfig {

    /**
     * 签名的key,由开发者约定
     */
    private static final String SINGING_KEY = "lst";

    /**
     * JWT令牌存储方案
     */
    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * Jwt令牌转换器
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SINGING_KEY);
        return converter;
    }

    /**
     * Jwt内容增强器
     */
    @Bean
    public JwtTokenEnhancer tokenEnhancer() {
        return new JwtTokenEnhancer();
    }
}

在授权服务器中配置jwt内容增强器信息

package com.srgstart.springsecurityoauth2demo.config;

import com.srgstart.springsecurityoauth2demo.service.UserDetailServiceImpl;
import io.jsonwebtoken.Jwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import java.util.ArrayList;
import java.util.List;

/**
 * @author srgstart
 * @create 2024/02/27 19:40
 * @description 授权服务器配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserDetailServiceImpl userDetailService;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    @Qualifier(value = "jwtTokenStore")
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;
    @Autowired
    private JwtTokenEnhancer tokenEnhancer;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 这里为了演示,把client-id 和secret存在内存中,正常情况下是从授权服务器中注册出来的
        clients.inMemory()
                .withClient("admin")
                .secret(passwordEncoder.encode("112233"))
                // 配置令牌的有效期(秒)
//                .accessTokenValiditySeconds(3600)
                // 配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                // 配置申请的权限范围
                .scopes("all")
                // 配置授权模式:授权码模式
                .authorizedGrantTypes("authorization_code");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 配置Jwt内容增强器
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(tokenEnhancer);
        delegates.add(accessTokenConverter);
        enhancerChain.setTokenEnhancers(delegates);

        endpoints
                //认证管理器
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailService)
                // 配置存储令牌策略
                .tokenStore(tokenStore)
                // 实现accessToken和JwtToken的转换
                .accessTokenConverter(accessTokenConverter)
                // jwt内容增强器
                .tokenEnhancer(enhancerChain);


    }
}

解析Jwt内容

@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 签名的key,由开发者约定
     */
    private static final String SINGING_KEY = "lst";

    /**
     * 获取当前用户
     * @param authentication
     * @return
     */
    @GetMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
        String head = request.getHeader("Authorization");
        String token = head.substring(head.indexOf("bearer") + 7);
        return Jwts.parser()
                .setSigningKey(SINGING_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
}

刷新令牌

简化了令牌过期后,重新申请授权码繁琐的过程

授权服务器配置

package com.srgstart.springsecurityoauth2demo.config;

import com.srgstart.springsecurityoauth2demo.service.UserDetailServiceImpl;
import io.jsonwebtoken.Jwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import java.util.ArrayList;
import java.util.List;

/**
 * @author srgstart
 * @create 2024/02/27 19:40
 * @description 授权服务器配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserDetailServiceImpl userDetailService;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    @Qualifier(value = "jwtTokenStore")
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;
    @Autowired
    private JwtTokenEnhancer tokenEnhancer;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 这里为了演示,把client-id 和secret存在内存中,正常情况下是从授权服务器中注册出来的
        clients.inMemory()
                .withClient("admin")
                .secret(passwordEncoder.encode("112233"))
                // 配置令牌的有效期(秒)
//                .accessTokenValiditySeconds(3600)
                // 配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                // 配置申请的权限范围
                .scopes("all")
                // 配置授权模式:授权码模式+刷新令牌
                .authorizedGrantTypes("authorization_code", "refresh_token");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 配置Jwt内容增强器
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(tokenEnhancer);
        delegates.add(accessTokenConverter);
        enhancerChain.setTokenEnhancers(delegates);

        endpoints
                //认证管理器
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailService)
                // 配置存储令牌策略
                .tokenStore(tokenStore)
                // 实现accessToken和JwtToken的转换
                .accessTokenConverter(accessTokenConverter)
                // jwt内容增强器
                .tokenEnhancer(enhancerChain);


    }
}

image.png

SpringSecurityOauth2整合SSO

授权步骤是在登录之后,登录之后进入授权层
默认访问资源服务器中的资源(接口),需要授权步骤,可以通过配置设置为自动授权和修改授权成功后跳转的uri。开启自动授权后,我们在直接访问资源服务器中的资源时,如果没有登录,跳到登录页面,登录后隐性的自动授权,授权成功后会跳到指定的uri,这样就不显示授权步骤。
访问资源服务器配置中的资源(接口)一定是在授权之后,才能调用。而不是授权服务器中配置的资源(接口),不用授权,但是需要登录,没有登录就会自动跳转到登录页面。
网关的作用:
1、做所有微服务的映射
2、配置资源服务器
3、自定义ZuulFilter过滤器实现登录鉴权

在UAA微服务中的授权服务器中配置

package com.srgstart.springsecurityoauth2demo.config;

import com.srgstart.springsecurityoauth2demo.service.UserDetailServiceImpl;
import io.jsonwebtoken.Jwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import java.util.ArrayList;
import java.util.List;

/**
 * @author srgstart
 * @create 2024/02/27 19:40
 * @description 授权服务器配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserDetailServiceImpl userDetailService;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    @Qualifier(value = "jwtTokenStore")
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;
    @Autowired
    private JwtTokenEnhancer tokenEnhancer;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 这里为了演示,把client-id 和secret存在内存中,正常情况下是从授权服务器中注册出来的
        clients.inMemory()
                .withClient("admin")
                .secret(passwordEncoder.encode("112233"))
                // 配置访问token的有效期(秒)
                .accessTokenValiditySeconds(3600)
                // 配置刷新令牌的有效期
                .refreshTokenValiditySeconds(86400)
                // 配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://localhost:8081/login")
                // 开启自动授权
                .autoApprove(true)
                // 配置申请的权限范围
                .scopes("all")
                // 配置授权模式:授权码模式+刷新令牌
                .authorizedGrantTypes("authorization_code", "refresh_token");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 配置Jwt内容增强器
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(tokenEnhancer);
        delegates.add(accessTokenConverter);
        enhancerChain.setTokenEnhancers(delegates);

        endpoints
                //认证管理器
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailService)
                // 配置存储令牌策略
                .tokenStore(tokenStore)
                // 实现accessToken和JwtToken的转换
                .accessTokenConverter(accessTokenConverter)
                // jwt内容增强器
                .tokenEnhancer(enhancerChain);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 获取密钥需要身份认证,使用单点登录时必须配置
        security.tokenKeyAccess("isAuthenticated()");
    }
}

调用者微服务中:
引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!-- 整合jwt,使用jwt的token代替oauth的token(实现accessToken和JwtToken的转换)-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

编写配置文件

server.port=8081
# 防止Cookie冲突,冲突会导致登录验证不通过
server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONIDO1
# 授权服务器地址
oauth2-server-url: http://localhost:8080
# 与授权服务器对应的配置
security.oauth2.client.client-id=admin
security.oauth2.client.client-secret=112233
# 授权服务器授权的uri
security.oauth2.client.user-authorization-uri=${oauth2-server-url}/oauth/authorize
# 获取token的uri
security.oauth2.client.access-token-uri=${oauth2-server-url}/oauth/token
# 获取jwt token的uri
security.oauth2.resource.jwt.key-uri=${oauth2-server-url}/oauth/token_key

在启动类上开启单点登录
image.png
编写资源
image.png
访问:http://localhost:8081/user/getCurrentUser

Spring Security是一个功能强大的安全框架,用于在Java应用程序中实现身份验证和授权。JWT(JSON Web Token)是一种轻量级的身份验证和授权机制,其中包含了验证用户身份的加密信息。OAuth 2.0是一种开放标准的授权协议,它允许用户授权第三方应用程序访问受保护的资源。 Spring Security可以与JWTOAuth 2.0结合使用,以提供更强大的身份验证和授权功能。使用JWT作为身份验证机制,可以在用户登录成功后生成一个JWT令牌,并将其加入到HTTP请求的Header中。服务端可以使用JWT中的信息,如用户名和权限,对请求进行验证,确保用户的身份是有效的。而OAuth 2.0允许用户通过授权服务器颁发的token来访问受保护的资源,Spring Security可以集成OAuth 2.0来实现授权验证的逻辑。 通过使用Spring Security结合JWTOAuth 2.0,可以轻松实现可伸缩、安全的身份验证和授权机制。开发人员可以使用Spring Security提供的各种功能,如用户认证、角色授权和访问控制,来保护应用程序中的敏感操作和数据。此外,使用JWTOAuth 2.0,可以实现无状态的API身份验证和授权,提高系统的可扩展性和性能。 总之,Spring SecurityJWTOAuth 2.0的结合为应用程序提供了安全、可靠的身份验证和授权机制。开发人员可以根据具体的需求配置和使用这些功能,以保护应用程序的安全和数据的机密性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值