Springboot 3 + Spring Security 6 + OAuth2 入门级最佳实践
当我的项目基于 SpringBoot 3 而我想使用Spring Security,最终不幸得到WebSecurityConfigurerAdapter被废弃的消息。本文档就是在这样的情况下产生的。
开发环境
应该基于:
- SpringBoot 3.x版本
- JDK 17
添加依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId><version>1.0.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
基本启动
在浏览器访问默认8080端口可以得到默认授权页面:
用户名为user,密码在控制台中自动生成:
写一个测试api:
@RestController@RequestMapping("/api")publicclass testController {@GetMapping("/hello")publicResponseEntityhello(){returnResponseEntity.ok("hello,this is my api");}}
登录后即可正常访问:
也可以退出登录:
Basic Auth授权流程
一个最基础的授权流程图:
新建一个SecurityConfig类:
// 使用@EnableWebSecurity注解开启Spring Security功能@EnableWebSecuritypublicclassSecurityConfig{// 定义一个SecurityFilterChain bean,用于配置安全过滤器链@BeanpublicSecurityFilterChainsecurityFilterChain(HttpSecurity http)throwsException{
http
// 配置授权请求规则.authorizeRequests()// 任何请求都需要认证.anyRequest().authenticated()// 使用and()方法连接多个配置.and()// 开启HTTP基本认证功能.httpBasic();return http.build();}}
可在API测试工具(此处为ApiFox)得到如下结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mW7aKqq2-1683119820517)(null)]
JWT身份验证过滤器
添加依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.4.0-b180830.0359</version></dependency>
- io.jsonwebtoken:jjwt依赖是Java JWT(JSON Web Token)库,它提供了一种方便的方法来生成、解析和验证JWT。在本例中,该依赖项用于生成和解析JWT,并提供了一些常用的JWT功能,如设置JWT的过期时间、签名和验证等。
- javax.xml.bind:jaxb-api依赖是Java体系结构与XML绑定(Java Architecture for XML Binding,JAXB)API的一部分,它提供了一种将Java对象与XML文档相互转换的方法。在本例中,jjwt库依赖了javax.xml.bind包,因此需要将其添加到项目中以解决可能的编译错误。
新建一个JwtUtils,帮助我们进行Jwt令牌生成与解析
importio.jsonwebtoken.Claims;importio.jsonwebtoken.Jwts;importio.jsonwebtoken.SignatureAlgorithm;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.stereotype.Component;importjava.util.Date;importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.TimeUnit;importjava.util.function.Function;@ComponentpublicclassJwtUtils{privateString jwtSigningKey ="secret";/**
* 从JWT中提取用户名
*/publicStringextractUsername(String token){returnextractClaim(token,Claims::getSubject);}/**
* 从JWT中提取过期时间
*/publicDateextractExpiration(String token){returnextractClaim(token,Claims::getExpiration);}/**
* 检查JWT是否包含指定的声明
*/publicbooleanhasClaim(String token,String claimName){finalClaims claims =extractAllClaims(token);return claims.get(claimName)!=null;}/**
* 从JWT中提取指定声明
*/public<T>TextractClaim(String token,Function<Claims,T> claimsResolver){finalClaims claims =extractAllClaims(token);return claimsResolver.apply(claims);}/**
* 从JWT中提取所有声明
*/publicClaimsextractAllClaims(String token){try{returnJwts.parser().setSigningKey(jwtSigningKey).parseClaimsJws(token).getBody();}catch(Exception e){
e.printStackTrace();// return a default Claims object or null}returnnull;}/**
* 检查JWT是否已过期
*/publicBooleanisTokenExpired(String token){Date expirationDate =extractExpiration(token);if(expirationDate ==null){returntrue;// or false based on your requirements}return expirationDate.before(newDate());}/**
* 生成JWT
*/publicStringgenerateToken(UserDetails userDetails){Map<String,Object> claims =newHashMap<>();returncreateToken(claims,userDetails);}/**
* 生成带有指定声明的JWT
*/publicStringgenerateToken(UserDetails userDetails,Map<String,Object>claims){returncreateToken(claims,userDetails);}/**
* 创建JWT
*/publicStringcreateToken(Map<String,Object> claims,UserDetails userDetails){returnJwts.builder().setClaims(claims).setSubject(userDetails.getUsername()).claim("authorities",userDetails.getAuthorities()).setIssuedAt(newDate(System.currentTimeMillis())).setExpiration(newDate(System.currentTimeMillis()+TimeUnit.HOURS.toMillis(24))).signWith(SignatureAlgorithm.HS256,jwtSigningKey).compact();}/**
* 验证JWT是否有效
*/publicBooleanisTokenValid(String token,UserDetails userDetails){finalString username =extractUsername(token);return(username.equals(userDetails.getUsername())&&!isTokenExpired(token));}}
generateToken()和createToken()是生成JWT的核心方法,它们使用了Jwts.builder()来构建JWT,并设置了一些常用的JWT功能,如设置JWT的过期时间、签名和验证等。isTokenValid()方法用于验证JWT是否有效,它检查JWT的用户名是否与用户详细信息中的用户名匹配,并检查JWT是否已过期。其他方法都是用于辅助功能的方法,用于从JWT中提取相关信息或检查JWT是否包含指定的声明。
创建Dao,并配合Security一起使用。理论上,此处可以配合MySQL与Redis一起使用,但这并非本文重点
importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.stereotype.Repository;importjava.util.Arrays;importjava.util.Collections;importjava.util.List;@RepositorypublicclassUserDao{// 在内存中存储应用程序的用户信息,这里只有一个用户privatefinalstaticList<UserDetails> APPLICATION_USERS =Arrays.asList(newUser("[email protected]",// 用户名"password",// 密码Collections.singleton(newSimpleGrantedAuthority("ROLE_USER"))// 用户角色));// 根据用户邮箱查找用户publicUserDetailsfindUserByEmail(String email){return APPLICATION_USERS
.stream().filter(u-> u.getUsername().equals(email))// 使用 Lambda 表达式过滤用户.findFirst()// 返回第一个匹配的用户.orElseThrow(()->newUsernameNotFoundException("No user was found"));// 如果没有匹配的用户,则抛出异常}}
创建JWT身份认证过滤器
@Component@RequiredArgsConstructorpublicclassJwtAthFilterextendsOncePerRequestFilter{privatefinalUserDao userDao;privatefinalJwtUtils jwtUtils;@OverrideprotectedvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain)throwsServletException,IOException{// 获取 HTTP 请求头部中的 Authorization 字段finalString authHeader = request.getHeader(AUTHORIZATION);finalString userEmail;finalString jwtToken;// 如果 Authorization 字段不存在或者不符合 Bearer Token 的格式,则跳过该过滤器if(authHeader ==null||!authHeader.startsWith("Bearer ")){
filterChain.doFilter(request, response);return;}// 提取 JWT Token,并从中获取用户邮箱
jwtToken = authHeader.substring(7);
userEmail = jwtUtils.extractUsername(jwtToken);// 如果用户邮箱不为空且未进行身份验证if(userEmail !=null&&SecurityContextHolder.getContext().getAuthentication()==null){// 根据用户邮箱从 UserDao 中查找用户UserDetails userDetails = userDao.findUserByEmail(userEmail);// 如果 JWT Token 有效,则进行身份验证if(jwtUtils.isTokenValid(jwtToken, userDetails)){UsernamePasswordAuthenticationToken authToken =newUsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
authToken.setDetails(newWebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authToken);}}// 继续处理请求
filterChain.doFilter(request, response);}}
该过滤器用于检查 HTTP 请求头部中是否包含 JWT Token,如果存在则从中提取出用户邮箱并进行身份验证。具体而言,该过滤器首先提取 HTTP 请求头部中的 Authorization 字段,检查其是否符合 Bearer Token 的格式。如果不符合,该过滤器将直接跳过,继续处理请求。如果 Authorization 字段符合 Bearer Token 的格式,则该过滤器将提取 JWT Token,并从中获取用户邮箱。如果用户邮箱不为空且未进行身份验证,则该过滤器将从 UserDao 中查找该用户,并使用 JwtUtils 类的 isTokenValid 方法验证 JWT Token 是否有效。如果 JWT Token 有效,则该过滤器将创建一个 UsernamePasswordAuthenticationToken 对象,并将其添加到 SecurityContextHolder 中,以进行身份验证。最后,该过滤器将继续处理请求。
配置安全过滤链
Q:为什么不使用antMatchers?
A:可参考这篇官方文档,Security5.8以上版本删除了过往常用的大量写法。
@Configuration@EnableWebSecurity@RequiredArgsConstructorpublicclassSecurityConfig{privatefinalJwtAthFilter jwtAthFilter;privatefinalUserDao userDao;// 定义一个 SecurityFilterChain bean,用于配置安全过滤器链@BeanpublicSecurityFilterChainsecurityFilterChain(HttpSecurity http)throwsException{
http
// 配置授权请求规则.csrf().disable().authorizeRequests()//认证请求无需授权.requestMatchers("/api/auth/**").permitAll()// 任何请求都需要授权.anyRequest().authenticated()// 使用 and() 方法连接多个配置.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authenticationProvider(authenticationProvider()).addFilterBefore(jwtAthFilter,UsernamePasswordAuthenticationFilter.class);return http.build();}// 配置 AuthenticationProvider bean@BeanpublicAuthenticationProviderauthenticationProvider(){finalDaoAuthenticationProvider authenticationProvider =newDaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());return authenticationProvider;}// 配置 AuthenticationManager bean@BeanpublicAuthenticationManagerauthenticationManager(AuthenticationConfiguration config)throwsException{return config.getAuthenticationManager();}// 配置密码编码器 bean@BeanpublicPasswordEncoderpasswordEncoder(){//return new BCryptPasswordEncoder();returnNoOpPasswordEncoder.getInstance();}// 配置 UserDetailsService bean@BeanpublicUserDetailsServiceuserDetailsService(){returnnewUserDetailsService(){@OverridepublicUserDetailsloadUserByUsername(String email)throwsUsernameNotFoundException{return userDao.findUserByEmail(email);}};}}
创建一个测试用实体类
@Getter@Setter@NoArgsConstructorpublicclassAuthenticationRequest{privateString email;privateString password;}
创建一个测试用授权认证控制器
@RestController@RequestMapping("/api/auth")@RequiredArgsConstructorpublicclassAuthenticationController{privatefinalAuthenticationManager authenticationManager;privatefinalUserDao userDao;privatefinalJwtUtils jwtUtils;@PostMapping("/login")publicResponseEntity<String>authenticate(@RequestBodyAuthenticationRequest request
){
authenticationManager.authenticate(newUsernamePasswordAuthenticationToken(request.getEmail(),request.getPassword()));finalUserDetails user = userDao.findUserByEmail(request.getEmail());if(user !=null){returnResponseEntity.ok(jwtUtils.generateToken(user));}returnResponseEntity.status(400).body("Some error has occurred");}}
一例测试:
令牌解析:
解析地址:JSON Web Tokens - jwt.io
Github作为授权服务器
可以使用第三方服务作为授权服务器。Spring Security 6 内置了Github、Google、FaceBook、OKTA的支持。
您可以选择在此处获取Github的支持,以注册一个属于您自身的全新OAuth应用程序。
创建一个基于OAuth授权认证的客户端程序
一如既往,该程序基于Springboot 3.x版本。
需要在此基础上添加由Springboot管理的如下核心依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
在您的配置文件进行如下基本配置:
server:port:8080spring:security:oauth2:client:registration:github:clientId: 515419724890eea8f1be
clientSecret:***************7128f77d
您应该将客户端ID及其密钥替换为您自身在Github上创建OAuth应用程序所获取的。
然后,我们可以创建并配置一个基本的安全过滤链:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeHttpRequests()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
;
return http.build();
}
}
作为开发环境的测试,这个安全过滤链实现了CSRF攻击防护的禁用、对应用程序的每一个URL进行身份验证,启用OAuth2登录。
可以考虑写一个测试用http端点:
@GetMapping("/hello")publicStringloginResult(){return"hello,this is my api";}
启动该客户端程序,您将会得到如下授权界面:
授权登录后请求我们刚刚编写的http端点,可以在浏览器得到:
hello,this is my api
通常,您可以考虑在Github中删除授权token。
自定义授权服务器
您也可以使用您所创建的授权服务器来处理您所创建的客户端进行授权认证。
首先,创建一个auth-service模块,该模块基于Springboot 3.x版本,用于创建授权服务器
您应该在此基础之上添加核心依赖:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId><version>1.0.2</version></dependency></dependencies>
编写授权服务器的安全过滤链:
importstaticorg.springframework.security.config.Customizer.withDefaults;importjava.security.KeyPair;importjava.security.KeyPairGenerator;importjava.security.interfaces.RSAPrivateKey;importjava.security.interfaces.RSAPublicKey;importjava.util.UUID;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.core.annotation.Order;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.crypto.password.NoOpPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.oauth2.core.AuthorizationGrantType;importorg.springframework.security.oauth2.core.ClientAuthenticationMethod;importorg.springframework.security.oauth2.core.oidc.OidcScopes;importorg.springframework.security.oauth2.jwt.JwtDecoder;importorg.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;importorg.springframework.security.oauth2.server.authorization.client.RegisteredClient;importorg.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;importorg.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;importorg.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;importorg.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;importorg.springframework.security.provisioning.InMemoryUserDetailsManager;importorg.springframework.security.web.SecurityFilterChain;importorg.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;importcom.nimbusds.jose.jwk.JWKSet;importcom.nimbusds.jose.jwk.RSAKey;importcom.nimbusds.jose.jwk.source.JWKSource;importcom.nimbusds.jose.proc.SecurityContext;@SuppressWarnings("deprecation")//忽略过时警告@ConfigurationpublicclassSecurityConfig{@Bean@Order(1)//指定执行优先级publicSecurityFilterChainasSecurityFilterChain(HttpSecurity http)throwsException{OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);return http
//为 OAuth2 认证服务器添加 OIDC 支持.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(withDefaults()).and().exceptionHandling(e -> e
.authenticationEntryPoint(newLoginUrlAuthenticationEntryPoint("/login")))//当未经身份验证的用户尝试访问受保护的资源时,将用户重定向到Security的默认登录页面.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)//配置 OAuth2 资源服务器以使用 JWT 令牌进行身份验证.build();}@Bean@Order(2)//表单登录与身份验证的请求授权publicSecurityFilterChainappSecurityFilterChain(HttpSecurity http)throwsException{return http
.formLogin(withDefaults())//授权任何已认证的用户可以访问任何请求.authorizeHttpRequests(authorize ->authorize.anyRequest().authenticated()).build();}//该 Bean 提供了一个存储在内存中的用户@BeanpublicUserDetailsServiceuserDetailsService(){var user1 =User.withUsername("user").password("password").authorities("read").build();returnnewInMemoryUserDetailsManager(user1);}//以纯文本形式保存密码,实际开发应该实现 BCryptPasswordEncoder()@BeanpublicPasswordEncoderpasswordEncoder(){returnNoOpPasswordEncoder.getInstance();}/*
向OAuth2认证服务器注册一个客户端应用程序进行授权
该 Bean 提供了一个内存中的注册客户端存储,用于 OAuth2 认证服务器的客户端授权
客户端应使用此处设置的值作为配置项
*/@BeanpublicRegisteredClientRepositoryregisteredClientRepository(){//生成随机UUID作为客户端唯一标识,避免多个客户端时ID冲突RegisteredClient registeredClient =RegisteredClient.withId(UUID.randomUUID().toString()).clientId("client")//设置授权客户端ID.clientSecret("secret")//设置客户端密钥.scope(OidcScopes.OPENID)//设置客户端的范围,这里使用了 OpenID Connect 的标准范围/*
设置客户端的重定向 URI,当用户授权后,OAuth2 认证服务器将重定向到该 URI
由于OAuth2认证服务器的安全性设置,此处必须使用127.0.0.1
使用localhost会导致拒绝重定向
*/.redirectUri("http://127.0.0.1:8080/login/oauth2/code/myoauth2")//设置客户端的身份验证方法,这里使用了基本身份验证方法.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)//设置客户端的授权类型,这里使用了授权码授权类型.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).build();returnnewInMemoryRegisteredClientRepository(registeredClient);}//该Bean用于配置OAuth2认证服务器,该例中我们无需配置@BeanpublicAuthorizationServerSettingsauthorizationServerSettings(){returnAuthorizationServerSettings.builder().build();}//解码Jwt令牌@BeanpublicJwtDecoderjwtDecoder(JWKSource<SecurityContext> jwkSource){returnOAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}//提供Jwt令牌@BeanpublicJWKSource<SecurityContext>jwkSource(){RSAKey rsaKey =generateRsa();JWKSet jwkSet =newJWKSet(rsaKey);return(jwkSelector, securityContext)-> jwkSelector.select(jwkSet);}//固定写法,生成RSA密钥对publicstaticRSAKeygenerateRsa(){KeyPair keyPair =generateRsaKey();RSAPublicKey publicKey =(RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey =(RSAPrivateKey) keyPair.getPrivate();returnnewRSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();}staticKeyPairgenerateRsaKey(){KeyPair keyPair;try{KeyPairGenerator keyPairGenerator =KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();}catch(Exception ex){thrownewIllegalStateException(ex);}return keyPair;}}
在配置文件中可以指定9090端口便于测试:
server:
port: 9090
然后,创建client-service模块,用于创建我们的客户端。
同样,它基于 Springboot 3.x 版本,在此基础上,我们还需添加核心依赖:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency></dependencies>
配置安全过滤链:
importstaticorg.springframework.security.config.Customizer.withDefaults;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.web.SecurityFilterChain;@ConfigurationpublicclassSecurityConfig{@BeanSecurityFilterChainsecurityFilterChain(HttpSecurity http)throwsException{
http
//所有请求都需经过授权认证.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())//配置登录URL.oauth2Login(oauth2Login ->
oauth2Login.loginPage("/oauth2/authorization/myoauth2"))//使用默认客户端配置.oauth2Client(withDefaults());return http.build();}}
可以创建一个控制器,写一个http端点用于测试:
@GetMapping("/")publicStringwelcome(){return"<h1>Welcome!</h1>";}
在配置文件中编写配置项:
server:port:8080spring:security:oauth2:client:registration:myoauth2:provider: spring
client-id: client
client-secret: secret
scope:- openid
authorization-grant-type: authorization_code
redirect-uri: http://127.0.0.1:8080/login/oauth2/code/myoauth2
provider:spring:issuer-uri: http://localhost:9090
运行授权服务器,然后运行客户端。
尝试在浏览器地址栏输入http://localhost:8080
尝试登录:
Authorization Server - Resource Server and OAuth2 Client
现在,我们可以尝试将资源服务纳入授权管理。
创建Authorization Server
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId><version>1.0.2</version></dependency></dependencies>
配置安全过滤链:
importstaticorg.springframework.security.config.Customizer.withDefaults;importjava.security.KeyPair;importjava.security.KeyPairGenerator;importjava.security.interfaces.RSAPrivateKey;importjava.security.interfaces.RSAPublicKey;importjava.util.UUID;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.core.annotation.Order;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.crypto.password.NoOpPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.oauth2.core.AuthorizationGrantType;importorg.springframework.security.oauth2.core.ClientAuthenticationMethod;importorg.springframework.security.oauth2.core.oidc.OidcScopes;importorg.springframework.security.oauth2.jwt.JwtDecoder;importorg.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;importorg.springframework.security.oauth2.server.authorization.client.RegisteredClient;importorg.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;importorg.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;importorg.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;importorg.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;importorg.springframework.security.provisioning.InMemoryUserDetailsManager;importorg.springframework.security.web.SecurityFilterChain;importorg.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;importcom.nimbusds.jose.jwk.JWKSet;importcom.nimbusds.jose.jwk.RSAKey;importcom.nimbusds.jose.jwk.source.JWKSource;importcom.nimbusds.jose.proc.SecurityContext;@SuppressWarnings("deprecation")@ConfigurationpublicclassSecurityConfig{@Bean@Order(1)publicSecurityFilterChainasSecurityFilterChain(HttpSecurity http)throwsException{OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);return http
.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(withDefaults()).and().exceptionHandling(e -> e
.authenticationEntryPoint(newLoginUrlAuthenticationEntryPoint("/login"))).oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt).build();}@Bean@Order(2)publicSecurityFilterChainappSecurityFilterChain(HttpSecurity http)throwsException{return http
.formLogin(withDefaults()).authorizeHttpRequests(authorize ->authorize.anyRequest().authenticated()).build();}@BeanpublicUserDetailsServiceuserDetailsService(){var user1 =User.withUsername("user").password("password").authorities("read").build();returnnewInMemoryUserDetailsManager(user1);}@BeanpublicPasswordEncoderpasswordEncoder(){returnNoOpPasswordEncoder.getInstance();}@BeanpublicRegisteredClientRepositoryregisteredClientRepository(){RegisteredClient registeredClient =RegisteredClient.withId(UUID.randomUUID().toString()).clientId("client").clientSecret("secret").scope("read").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).redirectUri("http://127.0.0.1:8080/login/oauth2/code/myoauth2").clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).build();returnnewInMemoryRegisteredClientRepository(registeredClient);}@BeanpublicAuthorizationServerSettingsauthorizationServerSettings(){returnAuthorizationServerSettings.builder().build();}@BeanpublicJwtDecoderjwtDecoder(JWKSource<SecurityContext> jwkSource){returnOAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}@BeanpublicJWKSource<SecurityContext>jwkSource(){RSAKey rsaKey =generateRsa();JWKSet jwkSet =newJWKSet(rsaKey);return(jwkSelector, securityContext)-> jwkSelector.select(jwkSet);}publicstaticRSAKeygenerateRsa(){KeyPair keyPair =generateRsaKey();RSAPublicKey publicKey =(RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey =(RSAPrivateKey) keyPair.getPrivate();returnnewRSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();}staticKeyPairgenerateRsaKey(){KeyPair keyPair;try{KeyPairGenerator keyPairGenerator =KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();}catch(Exception ex){thrownewIllegalStateException(ex);}return keyPair;}}
编写配置文件:
server:
port: 9090
创建Resource Server
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency></dependencies>
编写配置文件:
server:port:8090spring:security:oauth2:resourceserver:jwt:issuer-uri: http://localhost:9090
编写安全过滤链:
importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.oauth2.jwt.JwtDecoders;importorg.springframework.security.web.SecurityFilterChain;@ConfigurationpublicclassSecurityConfig{//从配置文件中获取OAuth2 Jwt令牌签发者的uri@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")String issuerUri;@BeanSecurityFilterChainsecurityFilterChain(HttpSecurity http)throwsException{return http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()).oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(JwtDecoders.fromIssuerLocation(issuerUri)))).build();}}
可以写一个测试用http端点,表示该资源服务的资源:
@GetMapping("/")publicStringhome(){LocalDateTime time =LocalDateTime.now();return"Welcome Resource Server! - "+ time;}
创建OAuth2 Client
引入核心依赖:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency></dependencies>
此处引入了 SpringBoot 3 的新特性,替换了常用的OpenFeign调用其它服务的方式,转而使用Spring自身的声明式HTTP接口。webflux可以让你在调用其它服务时像写控制器一样轻松。
编写配置文件:
server:port:8080spring:security:oauth2:client:registration:myoauth2:provider: spring
client-id: client
client-secret: secret
scope:- openid
authorization-grant-type: authorization_code
redirect-uri: http://127.0.0.1:8080/login/oauth2/code/myoauth2
provider:spring:issuer-uri: http://localhost:9090
编写安全过滤链:
importstaticorg.springframework.security.config.Customizer.withDefaults;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.web.SecurityFilterChain;@ConfigurationpublicclassSecurityConfig{@BeanSecurityFilterChainsecurityFilterChain(HttpSecurity http)throwsException{
http
//所有请求都需经过授权认证.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())//配置登录URL.oauth2Login(oauth2Login ->
oauth2Login.loginPage("/oauth2/authorization/myoauth2"))//使用默认客户端配置.oauth2Client(withDefaults());return http.build();}}
编写一个Client类,用以调用资源服务的资源:
importorg.springframework.web.service.annotation.GetExchange;importorg.springframework.web.service.annotation.HttpExchange;@HttpExchange("http://localhost:8090")publicinterfaceWelcomeClient{@GetExchange("/")StringgetWelcome();}
编写该Client的配置类,配置使用 OAuth2 认证的 WebClient,并将其转换为代理对象,以便于进行远程调用。通过
OAuth2AuthorizedClientManager
接口管理 OAuth2 授权客户端的生命周期,确保 WebClient 的安全性和可靠性:
importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;importorg.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;importorg.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;importorg.springframework.security.oauth2.client.registration.ClientRegistrationRepository;importorg.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;importorg.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;importorg.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;importorg.springframework.web.reactive.function.client.WebClient;importorg.springframework.web.reactive.function.client.support.WebClientAdapter;importorg.springframework.web.service.invoker.HttpServiceProxyFactory;@ConfigurationpublicclassWebClientConfig{/**
* 创建名为 welcomeClient 的 bean,类型为 WelcomeClient。
* 使用 OAuth2AuthorizedClientManager 作为参数创建 HttpServiceProxyFactory,然后使用它创建客户端。
*
* @param authorizedClientManager 用于创建 HttpServiceProxyFactory 的 OAuth2AuthorizedClientManager 实例
* @return WelcomeClient 实例
* @throws Exception 如果创建客户端时发生错误,则抛出异常
*/@BeanpublicWelcomeClientwelcomeClient(OAuth2AuthorizedClientManager authorizedClientManager)throwsException{returnhttpServiceProxyFactory(authorizedClientManager).createClient(WelcomeClient.class);}/**
* 创建 HttpServiceProxyFactory,以便在创建客户端时使用。
*
* @param authorizedClientManager 用于创建 HttpServiceProxyFactory 的 OAuth2AuthorizedClientManager 实例
* @return 创建的 HttpServiceProxyFactory 实例
*/privateHttpServiceProxyFactoryhttpServiceProxyFactory(OAuth2AuthorizedClientManager authorizedClientManager){// 创建 ServletOAuth2AuthorizedClientExchangeFilterFunction,使用它来处理 OAuth2 认证ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =newServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);// 设置默认的 OAuth2 授权客户端
oauth2Client.setDefaultOAuth2AuthorizedClient(true);// 创建 WebClient,应用 OAuth2 认证配置WebClient webClient =WebClient.builder().apply(oauth2Client.oauth2Configuration()).build();// 创建 WebClientAdapter,它允许我们在创建客户端时使用 WebClientWebClientAdapter client =WebClientAdapter.forClient(webClient);// 创建 HttpServiceProxyFactory,它可用于创建客户端returnHttpServiceProxyFactory.builder(client).build();}/**
* 创建 OAuth2AuthorizedClientManager 的 bean。
*
* @param clientRegistrationRepository 用于管理客户端注册信息的 ClientRegistrationRepository 实例
* @param authorizedClientRepository 用于管理授权客户端信息的 OAuth2AuthorizedClientRepository 实例
* @return OAuth2AuthorizedClientManager 实例
*/@BeanpublicOAuth2AuthorizedClientManagerauthorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,OAuth2AuthorizedClientRepository authorizedClientRepository){// 创建 OAuth2AuthorizedClientProvider,用于获取授权客户端OAuth2AuthorizedClientProvider authorizedClientProvider =OAuth2AuthorizedClientProviderBuilder.builder().authorizationCode().refreshToken().build();// 创建 DefaultOAuth2AuthorizedClientManager,使用它来管理授权客户端DefaultOAuth2AuthorizedClientManager authorizedClientManager =newDefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);// 设置授权客户端提供程序
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);// 返回 OAuth2AuthorizedClientManagerreturn authorizedClientManager;}}
写一个测试用http端点:
@GetMapping("/")publicStringwelcome(){String welcome = welcomeClient.getWelcome();return"<h1>"+ welcome +"</h1>";}
然后运行授权服务、资源服务和客户端
如果你尝试访问资源服务,即 http://localhost:8090 ,会得到:
当前无法使用此页面
如果问题仍然存在,请联系网站所有者。
HTTP ERROR 401
这是因为资源服务的调用应通过客户端进行,而客户端获取资源服务,则需通过OAuth2认证授权。
接着,我们尝试访问客户端:
这将自动跳转至默认的授权页面,我们可以在这里进行登录,即可跳转至8080端口,通过客户端获取到资源服务:
Welcome Resource Server! - 2023-05-03T14:29:20.723645200
实现 BCryptPasswordEncoder密码加密
密码加密是一个常见应用。以刚刚创建的授权服务为例,一般来说,您可以在其基础上修改与增加如下代码:
// @Bean// public PasswordEncoder passwordEncoder() {// return NoOpPasswordEncoder.getInstance();// }@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}
@BeanpublicUserDetailsServiceuserDetailsService(){var user1 =User.withUsername("user").password(passwordEncoder().encode("password"))//修改此处.authorities("read").build();returnnewInMemoryUserDetailsManager(user1);}
@BeanpublicRegisteredClientRepositoryregisteredClientRepository(){RegisteredClient registeredClient =RegisteredClient.withId(UUID.randomUUID().toString()).clientId("client").clientSecret(passwordEncoder().encode("secret"))//修改此处.scope("read").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).redirectUri("http://127.0.0.1:8080/login/oauth2/code/myoauth2").clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).build();returnnewInMemoryRegisteredClientRepository(registeredClient);}
在数据库中自定义用户信息
您可以采用 JPA/Hibernate等一切你所熟悉的数据库框架,但这并不是本文档的重点。这里仅以MySQL 8.x与MybatisPlus为例:
延续上面写好的Demo,向您的授权服务添加如下依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
当然,我们需要在配置文件中配置数据库,如果您愿意观察授权服务运行情况,可以像我一样打开监控日志:
server:port:9090spring:datasource:type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/database?useSSL=false
username: root
password:123456mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:level:org:springframework:security: TRACE
提供如下由Navicat导出的三个SQL文件供您建表参考:
SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS =0;-- ------------------------------ Table structure for user-- ----------------------------DROPTABLEIFEXISTS`user`;CREATETABLE`user`(`id`intNOTNULLAUTO_INCREMENT,`name`varchar(255)CHARACTERSET utf8 COLLATE utf8_general_ci NOTNULL,`password`varchar(255)CHARACTERSET utf8 COLLATE utf8_general_ci NOTNULL,PRIMARYKEY(`id`)USINGBTREE)ENGINE=InnoDBCHARACTERSET= utf8 COLLATE= utf8_general_ci ROW_FORMAT = Dynamic;-- ------------------------------ Records of user-- ----------------------------INSERTINTO`user`VALUES(1,'root','$2a$12$.b6oalHafQC/beQaqXnwdeLY7M5KxL..is7kEguwQbHX3FPXfgRKW');SET FOREIGN_KEY_CHECKS =1;
user.sql
我们这里没有实现注册模块,但又想体验密码加密功能,因此推荐从此处由纯文本密码转换成加密密码存储至数据库中。此处定义的用户信息为:
字段值namerootpassword123456
SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS =0;-- ------------------------------ Table structure for authority-- ----------------------------DROPTABLEIFEXISTS`authority`;CREATETABLE`authority`(`id`intNOTNULLAUTO_INCREMENT,`authority`varchar(255)CHARACTERSET utf8 COLLATE utf8_general_ci NOTNULL,PRIMARYKEY(`id`)USINGBTREE)ENGINE=InnoDBCHARACTERSET= utf8 COLLATE= utf8_general_ci ROW_FORMAT = Dynamic;-- ------------------------------ Records of authority-- ----------------------------INSERTINTO`authority`VALUES(1,'ROLE_USER');INSERTINTO`authority`VALUES(2,'ROLE_ADMIN');INSERTINTO`authority`VALUES(3,'ROLE_DEVELOPER');SET FOREIGN_KEY_CHECKS =1;
authority.sql
SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS =0;-- ------------------------------ Table structure for user_authority-- ----------------------------DROPTABLEIFEXISTS`user_authority`;CREATETABLE`user_authority`(`id`intNOTNULLAUTO_INCREMENT,`user_id`intNOTNULL,`authority_id`intNOTNULL,PRIMARYKEY(`id`)USINGBTREE)ENGINE=InnoDBCHARACTERSET= utf8 COLLATE= utf8_general_ci ROW_FORMAT = Dynamic;-- ------------------------------ Records of user_authority-- ----------------------------INSERTINTO`user_authority`VALUES(1,1,1);SET FOREIGN_KEY_CHECKS =1;
user_authority.sql
编写授权服务
我们将注释安全过滤链中的以下代码,因为我们不再需要在内存中创建用户:
// @Bean// public UserDetailsService userDetailsService() {// var user1 = User.withUsername("user")// .password(passwordEncoder().encode("password"))// .authorities("read")// .build();// return new InMemoryUserDetailsManager(user1);// }
创建两个实体类其一:User
importlombok.AllArgsConstructor;// 导入 Lombok 库中的注解importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;importjava.util.List;@Data// 自动生成 getter 和 setter 方法@NoArgsConstructor// 自动生成无参构造函数@AllArgsConstructor// 自动生成所有参数的构造函数@Builder// 自动生成 Builder 类publicclassUser{privateInteger id;// 用户 IDprivateString name;// 用户名privateString password;// 密码// 使用 @Builder.Default 指定默认值@Builder.DefaultprivateBoolean accountNonExpired =true;// 账号是否未过期,默认为 [email protected] accountNonLocked =true;// 账号是否未锁定,默认为 [email protected] credentialsNonExpired =true;// 凭证是否未过期,默认为 [email protected] enabled =true;// 账号是否可用,默认为 true}
创建两个实体类其二:Authority
importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;importlombok.NonNull;@Data@NoArgsConstructor@AllArgsConstructor@BuilderpublicclassAuthority{privateInteger id;@NonNullprivateString authority;}
创建两个Mapper接口其一:UserMapper
importcom.baomidou.mybatisplus.core.mapper.BaseMapper;importcom.weather.entity.User;importorg.apache.ibatis.annotations.Mapper;@MapperpublicinterfaceUserMapperextendsBaseMapper<User>{}
创建两个Mapper接口其二:AuthorityMapper
importcom.baomidou.mybatisplus.core.mapper.BaseMapper;importcom.weather.entity.Authority;importorg.apache.ibatis.annotations.Mapper;@MapperpublicinterfaceAuthorityMapperextendsBaseMapper<Authority>{}
现在,我们应该定义一个表示用户信息的类,并实现了 UserDetails 接口中的方法,以便于在 Spring Security 框架中对用户进行认证和授权:
importjava.util.Collection;importjava.util.List;importjava.util.stream.Collectors;importcom.weather.entity.Authority;importcom.weather.entity.User;importlombok.Data;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.UserDetails;importlombok.RequiredArgsConstructor;@Data// 自动生成 getter、setter、equals、hashCode 等方法@RequiredArgsConstructor// 为 final 属性生成带参构造函数publicclassMyUserDetailsimplementsUserDetails{privatestaticfinallong serialVersionUID =1L;privatefinalUser user;// 用户信息privatefinalList<Authority> authority;// 用户权限列表// 实现 UserDetails 接口中的方法@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){return authority.stream()// 将权限列表转换为流.map(auth ->newSimpleGrantedAuthority(auth.getAuthority()))// 将 Authority 对象转换为 GrantedAuthority 对象.collect(Collectors.toSet());// 将转换后的对象集合转换为 Set 类型并返回}@OverridepublicStringgetPassword(){return user.getPassword();}@OverridepublicStringgetUsername(){return user.getName();}@OverridepublicbooleanisAccountNonExpired(){return user.getAccountNonExpired();}@OverridepublicbooleanisAccountNonLocked(){return user.getAccountNonLocked();}@OverridepublicbooleanisCredentialsNonExpired(){return user.getCredentialsNonExpired();}@OverridepublicbooleanisEnabled(){return user.getEnabled();}}
现在我们需要创建一个用户信息存储库类,以存储来自数据库的用户信息。首先实现一个UserRepository接口:
importcom.weather.entity.Authority;importcom.weather.entity.User;importjava.util.List;publicinterfaceUserRepository{UsergetUserByName(String name);List<Authority>getAuthoritiesByUserId(int userID);}
接下来,您需要写一个该存储库的实现类:
importcom.baomidou.mybatisplus.core.conditions.query.QueryWrapper;importcom.weather.entity.Authority;importcom.weather.entity.User;importcom.weather.mapper.AuthorityMapper;importcom.weather.mapper.UserMapper;importlombok.RequiredArgsConstructor;importorg.springframework.stereotype.Service;importjava.util.List;@Service@RequiredArgsConstructorpublicclassUserRepositoryImplimplementsUserRepository{privatefinalUserMapper userMapper;privatefinalAuthorityMapper authorityMapper;@OverridepublicUsergetUserByName(String name){return userMapper.selectOne(newQueryWrapper<User>().eq("name",name).select("id","name","password"));}@OverridepublicList<Authority>getAuthoritiesByUserId(int userID){return authorityMapper.selectList(newQueryWrapper<Authority>().inSql("id","select authority_id from user_authority where user_id = "+ userID).select("id","authority"));}}
最终,我们需要将数据库中获取的用户信息传入UserDetails:
importcom.weather.entity.Authority;importcom.weather.entity.User;importcom.weather.model.MyUserDetails;importcom.weather.repository.UserRepository;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.stereotype.Service;importlombok.RequiredArgsConstructor;importjava.util.List;importjava.util.Optional;@Service@RequiredArgsConstructorpublicclassMyUserDetailServiceimplementsUserDetailsService{privatefinalUserRepository userRepository;@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{User user = userRepository.getUserByName(username);List<Authority> authorities = userRepository.getAuthoritiesByUserId(user.getId());returnOptional.ofNullable(user).map(u ->newMyUserDetails(u, authorities)).orElseThrow(()->newUsernameNotFoundException("User not found"));}}
现在,我们就可以使用数据库所定义的用户进行登录。
自定义OAuth2令牌:
在授权服务的安全过滤链上增加如下代码:
@BeanOAuth2TokenCustomizer<JwtEncodingContext>tokenCustomizer(){return context ->{Authentication principal = context.getPrincipal();if(context.getTokenType().getValue().equals("id_token")){
context.getClaims().claim("Test","Test Id Token");}if(context.getTokenType().getValue().equals("access_token")){
context.getClaims().claim("Test","Test Access Token");Set<String> authorities = principal.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
context.getClaims().claim("authorities", authorities).claim("user", principal.getName());}};}
Jwt令牌身份验证转换器
我们现在可以考虑实现将JWT令牌转换为身份验证对象,以便在Spring Security中进行身份验证和授权。
首先,我们在资源服务器的安全过滤链中编写如下代码,返回一个身份验证转换对象:
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
//用于从JWT令牌中提取授权信息并将其转换为GrantedAuthority对象的集合
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
//JWT令牌中的授权信息在名为"authorities"的声明中
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
//授权信息中不包含前缀
grantedAuthoritiesConverter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
这将使Jwt令牌中的身份写入Authentication对象中,进而可供调用。
写一个测试用http端点:
@GetMapping("/")publicStringhome(Authentication authentication){LocalDateTime time =LocalDateTime.now();return"Welcome ResourceServer! - "+ time +"<br>"+ authentication.getName()+" - "+ authentication.getAuthorities();}
身份验证通过后,您将得到如下结果:
Welcome ResourceServer! - 2023-05-03T19:47:27.225993700
root - [ROLE_USER]
实现客户端登录后获取权限信息
在客户端的安全过滤链中编写以下代码:
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
OidcUser oidcUser = delegate.loadUser(userRequest);
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
try {
JWT jwt = JWTParser.parse(accessToken.getTokenValue());
JWTClaimsSet claimSet = jwt.getJWTClaimsSet();
Collection<String> userAuthorities = claimSet.getStringListClaim("authorities");
mappedAuthorities.addAll(userAuthorities.stream()
.map(SimpleGrantedAuthority::new)
.toList());
} catch (ParseException e) {
System.err.println("Error OAuth2UserService: " + e.getMessage());
}
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
该方法首先使用
delegate
对象(即
OidcUserService
)来加载用户信息。然后从
userRequest
对象中获取到
OAuth2AccessToken
并解析出其中的 JWT,从中提取出用户的权限信息。最后,将用户信息和权限信息封装成一个新的
OidcUser
对象,用于后续的认证和授权。
OidcUser
是Spring Security框架中的一个接口,用于表示OpenID Connect(OIDC)认证成功后的用户信息。在OAuth 2.0和OIDC授权流程中,用户通过认证服务器进行身份验证,并在认证成功后,认证服务器会返回一个包含用户信息的JWT令牌。
OidcUser
接口用于表示这个JWT令牌中包含的用户信息。
这里同样会将用户信息存入Authentication对象中。因为用户信息是在
oauth2UserService()
方法中被处理的,最终会被封装成一个
OidcUser
对象。
OidcUser
实现了 Spring Security 的
Authentication
接口,因此可以将其作为认证信息存储在
SecurityContext
中,用于后续的授权访问。
@BeanSecurityFilterChainsecurityFilterChain(HttpSecurity http,ClientRegistrationRepository clientRegistrationRepository)throwsException{String base_uri =OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;DefaultOAuth2AuthorizationRequestResolver resolver =newDefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, base_uri);
resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
http
//所有请求都需经过授权认证.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())//配置登录URL.oauth2Login(oauth2Login ->{
oauth2Login.loginPage("/oauth2/authorization/myoauth2");
oauth2Login.authorizationEndpoint().authorizationRequestResolver(resolver);
oauth2Login.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService()));})//使用默认客户端配置.oauth2Client(withDefaults());return http.build();}
这个方法接收一个
HttpSecurity
对象,用于定义HTTP请求的安全配置,以及一个
ClientRegistrationRepository
对象,用于存储第三方认证服务的客户端配置。
这个方法主要完成以下配置:
- 对所有请求进行授权认证,要求用户登录。
- 设置OAuth 2.0登录的URL。
- 配置OAuth 2.0的授权端点,使用客户端的PKCE(Proof Key for Code Exchange)来增加安全性。
- 设置OIDC(OpenID Connect)用户服务,用于获取用户权限信息。
- 使用默认的OAuth 2.0客户端配置。
写一个测试用http端点:
@GetMapping("/")publicStringwelcome(Authentication authentication){String authorities = authentication.getName()+" - "+ authentication.getAuthorities().toString();String welcome = welcomeClient.getWelcome();return"<h1>"+ welcome +"</h1><h2>"+ authorities +"</h2>";}
授权认证成功后,将得到如下内容:
Welcome ResourceServer! - 2023-05-03T19:59:44.575374
root - [ROLE_USER]
root - [ROLE_USER]
获取刷新令牌
一般来说,用户登录后会得到一个令牌,这个令牌在某种情况下过期或销毁后,用户需要重新登录以获取令牌。而刷新令牌的设计弥补了这一点。您可以通过刷新令牌,在旧令牌过期或销毁后获得新的令牌,以维持您的登录状态,并保持安全性,而无需重新登录获取令牌。这在手机A应用(您会发现大多数手机应用在登录一次后会始终保持着登录状态)非常常见。一些第三方授权服务也会使用刷新令牌以延长用户访问令牌的有效期。
编写客户端的控制器
可以在控制器中编写下列代码:
privatefinalWelcomeClient welcomeClient;privatefinalOAuth2AuthorizedClientService oAuth2AuthorizedClientService;@GetMapping("/")publicStringwelcome(Authentication authentication){String authorities = authentication.getName()+" - "+ authentication.getAuthorities().toString();String welcome = welcomeClient.getWelcome();return"<h1>"+ welcome +"</h1><h2>"+ authorities +"</h2>";}@GetMapping("/token")publicStringtoken(Authentication authentication){//Authentication authentication = SecurityContextHolder.getContext().getAuthentication();OAuth2AuthenticationToken oAuth2AuthenticationToken =(OAuth2AuthenticationToken) authentication;OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService
.loadAuthorizedClient(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId(), oAuth2AuthenticationToken.getName());String jwtAccessToken = oAuth2AuthorizedClient.getAccessToken().getTokenValue();String jwtRefrechToken = oAuth2AuthorizedClient.getRefreshToken().getTokenValue();return"<b>JWT Access Token: </b>"+ jwtAccessToken +"<br/><br/><b>JWT Refresh Token: </b>"+ jwtRefrechToken;}@GetMapping("idtoken")publicStringidtoken(@AuthenticationPrincipalOidcUser oidcUser){OidcIdToken oidcIdToken = oidcUser.getIdToken();String idTokenValue = oidcIdToken.getTokenValue();return"<b>Id Token: </b>"+ idTokenValue;}
参考:
配置迁移 :Spring 安全性
Spring Security Tutorial - [NEW] [2023]
OAuth2 & Spring boot 3 & Social login | never been easier
Spring Boot 3 Tutorial Security - Oauth2 - Authorization Server - Resource Server and OAuth2 Client
e final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
@GetMapping("/")
public String welcome(Authentication authentication) {
String authorities = authentication.getName() + " - " + authentication.getAuthorities().toString();
String welcome = welcomeClient.getWelcome();
return "<h1>" + welcome + "</h1><h2>" + authorities + "</h2>";
}
@GetMapping("/token")
public String token(Authentication authentication) {
//Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken) authentication;
OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService
.loadAuthorizedClient(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId(), oAuth2AuthenticationToken.getName());
String jwtAccessToken = oAuth2AuthorizedClient.getAccessToken().getTokenValue();
String jwtRefrechToken = oAuth2AuthorizedClient.getRefreshToken().getTokenValue();
return "<b>JWT Access Token: </b>" + jwtAccessToken + "<br/><br/><b>JWT Refresh Token: </b>" + jwtRefrechToken;
}
@GetMapping("idtoken")
public String idtoken(@AuthenticationPrincipal OidcUser oidcUser) {
OidcIdToken oidcIdToken = oidcUser.getIdToken();
String idTokenValue = oidcIdToken.getTokenValue();
return "<b>Id Token: </b>" + idTokenValue;
}
参考:
配置迁移 :Spring 安全性
Spring Security Tutorial - [NEW] [2023]
OAuth2 & Spring boot 3 & Social login | never been easier
Spring Boot 3 Tutorial Security - Oauth2 - Authorization Server - Resource Server and OAuth2 Client
Configure OAuth2 Spring Authorization Server with JWT support | Sergey Kryvets Blog (skryvets.com)
版权归原作者 organwalk 所有, 如有侵权,请联系我们删除。