Spring Security OAuth2的基本使用
Spring Security OAuth2
Spring Security OAuth2对Oauth2进行了实现,主要包含认证服务器和资源服务器两大块的实现。
认证服务器
对四种授权模式的实现和Token的生成与存储,也可自定义获取Token的方式
资源服务器
在Spring Security过滤器链上加OAuth2AuthenticationProcessingFilter过滤器,让OAuth2协议发放令牌认证的方式来保护资源
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
配置认证服务器
自定义MyUser对象
@Data
public class MyUser implements Serializable {
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
/**
* 账号是否未过期
*/
private boolean accountNonExpired = true;
/**
* 账号是否未锁定
*/
private boolean accountNonLocked = true;
/**
* 凭证是否未过期
*/
private boolean credentialsNonExpired = true;
/**
* 账号是否启用
*/
private boolean enabled = true;
}
创建认证服务器
在Spring Security的配置类上使用@EnableAuthorizationServer注解申明
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义MyUserDetailService
自定义MyUserDetailService,实现账号登录相关校验逻辑,预设密码必须为123456,且拥有admin权限,使用任意账号即可登录。
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUser user = new MyUser();
user.setUserName(username);
user.setPassword(this.passwordEncoder.encode("123456"));
return new User(username, user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
配置application.yml
若不配置指定client-id和client-secret,则会随机分配
security:
oauth2:
client:
client-id: web
client-secret: 123456789
registered-redirect-uri: http://127.0.0.1:8888/test
启动项目,控制台打印出随机分配的client-id和client-secret
security.oauth2.client.client-id = 798f97b8-e9b0-4c87-88c2-6b35b9813966
security.oauth2.client.client-secret = 782fa08a-bec5-4d27-8269-7f7b35488143
若配置指定,则打印指定的参数
security.oauth2.client.client-id = web
security.oauth2.client.client-secret = ****
授权码模式获取令牌
向认证服务器请求授权码
http://localhost:8888/oauth/authorize?response_type=code&client_id=web&redirect_uri=http://127.0.0.1:8888/test&scope=all
response_type=code:表示授权码模式
client_id=web:配置文件中指定的web
redirect_uri:指定一个地址,用来重定向获取授权码
scope=all:表示所有权限
根据定义的UserDetailService逻辑,使用任意用户名,密码123456登录即可
注意:指定的redirect_uri必须同时在配置文件中指定,否则出现以下错误提示
登录成功后页面成功跳转到授权页面
选择Approve,点击Authorize按钮后,页面跳转到指定的redirect_uri,并带上授权码信息
通过授权码CODE从认证服务器获取令牌Token
grant_type:authorization_code
code:授权码
client_id:web
redirect_uri:http://127.0.0.1:8888/test
scope:all
请求头添加:key=Authorization,value=Basic加上client_id:client_secret
经过base64加密后的值
base64:https://1024tools.com/base64
确认参数无误后,发送请求,获取令牌Token
{
"access_token": "882698ab-7a08-4aaf-847c-11c2d191540d",
"token_type": "bearer",
"refresh_token": "ea709f2f-987a-4ecf-ae79-62267a453a10",
"expires_in": 43199,
"scope": "all"
}
注意:一个授权码只能换一次令牌,如果再次使用相同授权码Code获取令牌,将返回如下信息
{
"error": "invalid_grant",
"error_description": "Invalid authorization code: E6Het7"
}
密码模式获取令牌
密码模式获取令牌相对简单 ,直接发送Post请求:http://localhost:8888/oauth/token
请求头添加:key=Authorization,value=Basic加上client_id:client_secret经过base64加密后的值
发送请求,获取令牌
{
"access_token": "5983f2eb-d83d-4d5a-8e94-89cd417d360f",
"token_type": "bearer",
"refresh_token": "56b1d244-d5d5-4f3c-8597-7921f6d6c03b",
"expires_in": 43199,
"scope": "all"
}
配置资源服务器
配置资源服务器,让客户端可以通过合法的令牌来获取定义的资源。
若不配置资源服务器,直接使用Token(请求头添加key=Authorization,值=token_type access_token)访问资源,即使Token正确,也无法访问资源信息。将返回如下信息:
{
"timestamp": "2022-10-20T07:06:26.045+00:00",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/index"
}
定义资源
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("index")
public Object index(Authentication authentication) {
return authentication;
}
}
配置application.yml
user-info-uri
指定认证服务器地址和端口
security:
oauth2:
resource:
user-info-uri: http://127.0.0.1:8888
配置资源服务器
在配置类上使用@EnableResourceServer注解申明是一个资源服务器
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter{
}
执行测试
使用授权模式或者密码模式获取令牌,然后使用令牌请求资源地址:http://localhost:8888/index
,注意携带请求头。
使用错误access_token
{
"error": "invalid_token",
"error_description": "Invalid access token: 1d4379313-c2f3-4ba4-87ce-26371726ba62"
}
使用正确access_token
{
"authorities": [
{
"authority": "admin"
}
],
"details": {
"remoteAddress": "127.0.0.1",
"sessionId": null,
"tokenValue": "d4379313-c2f3-4ba4-87ce-26371726ba62",
"tokenType": "Bearer",
"decodedDetails": null
},
"authenticated": true,
"userAuthentication": {
"authorities": [
{
"authority": "admin"
}
],
"details": {
"grant_type": "password",
"username": "admin",
"scope": "all"
},
"authenticated": true,
"principal": {
"password": null,
"username": "admin",
"authorities": [
{
"authority": "admin"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"credentials": null,
"name": "admin"
},
"principal": {
"password": null,
"username": "admin",
"authorities": [
{
"authority": "admin"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"oauth2Request": {
"clientId": "web",
"scope": [
"all"
],
"requestParameters": {
"grant_type": "password",
"username": "admin",
"scope": "all"
},
"resourceIds": [],
"authorities": [
{
"authority": "ROLE_USER"
}
],
"approved": true,
"refresh": false,
"redirectUri": null,
"responseTypes": [],
"extensions": {},
"grantType": "password",
"refreshTokenRequest": null
},
"clientOnly": false,
"credentials": "",
"name": "admin"
}
注意事项
若同时定义认证服务器和资源服务器,使用授权码模式获取令牌可能会遇到异常:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
解决方案:使用@Order()
注解,只要确保认证服务器先于资源服务器配置即可
@Order(1)
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Order(2)
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
}
自定义账号密码登录获取令牌
使用账户、密码登录后,基于Spring Security OAuth2默认配置生成相应Token令牌。
自定义登录失败Handler
创建MyAuthenticationFailureHandler
类实现AuthenticationFailureHandler
接口,重写onAuthenticationFailure
方法,添加处理登录失败逻辑。
import com.alibaba.fastjson.JSONObject;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
// 设置响应状态码
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
// 设置响应内容格式
response.setContentType("application/json;charset=utf-8");
// 设置响应内容
response.getWriter().write(JSONObject.toJSONString(exception.getMessage()));
}
}
自定义登录成功Handler
创建MyAuthenticationSucessHandler
类实现AuthenticationSuccessHandler
接口,重写onAuthenticationSuccess
方法,添加处理登录成功逻辑,主要是生产Token令牌的过程。
@Component
@Slf4j
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// 从请求头中获取ClientId
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Basic ")) {
throw new BadCredentialsException("凭证有误");
}
String[] tokens = this.decoding(header);
String clientId = tokens[0];
String clientSecret = tokens[1];
TokenRequest tokenRequest;
// 使用ClientDetailsService获取ClientDetails
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
// 校验ClientId和ClientSecret的正确性
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("客户端 clientId:" + clientId + "对应信息不存在");
} else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
throw new UnapprovedClientAuthenticationException("客户端 Secret 不正确");
} else {
// 使用TokenRequest构造器生成TokenRequest
tokenRequest = new TokenRequest(new HashMap<>(), clientId, clientDetails.getScope(), "custom");
}
// 使用createOAuth2Request方法获取OAuth2Request
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
// 构造OAuth2Authentication
OAuth2Authentication auth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
// 生成OAuth2AccessToken
OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(auth2Authentication);
// 返回Token
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(token));
}
private String[] decoding(String header) {
// Basic d2ViOjEyMzQ1Njc4OQ==进行处理
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
// 进行解码
byte[] decoded = Base64.getDecoder().decode(base64Token);
// 转字符串,得到 web:123456789
String token = new String(decoded, StandardCharsets.UTF_8);
int res = token.indexOf(":");
if (res == -1) {
throw new BadCredentialsException("凭证有误");
} else {
return new String[]{token.substring(0, res), token.substring(res + 1)};
}
}
}
配置资源服务器
在资源服务器添加基本的Spring Security配置
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private MyAuthenticationSucessHandler authenticationSucessHandler;
@Autowired
private MyAuthenticationFailureHandler authenticationFailureHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.loginProcessingUrl("/login") // 表单登录URL
.successHandler(authenticationSucessHandler) // 处理登录成功
.failureHandler(authenticationFailureHandler) // 处理登录失败
.and()
.authorizeRequests() // 授权配置
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and()
.csrf().disable();
}
}
执行测试
访问请求:http://localhost:8888/login
接口
请求头添加:key=Authorization
,value=Basic加上client_id:client_secret
经过base64加密后的值
base64编码:https://1024tools.com/base64
发送POST请求,成功获取到令牌
{
"access_token": "84d19e68-9975-4182-a048-09c2b551f343",
"token_type": "bearer",
"refresh_token": "bfee81a9-bb53-4812-a50d-6b6a9488a00f",
"expires_in": 43197
}
使用令牌访问令/index
接口
{
"authorities": [
{
"authority": "admin"
}
],
"details": {
"remoteAddress": "127.0.0.1",
"sessionId": null,
"tokenValue": "ce5b625f-b2c9-444c-b2cd-2ed86b16a625",
"tokenType": "Bearer",
"decodedDetails": null
},
"authenticated": true,
"userAuthentication": {
"authorities": [
{
"authority": "admin"
}
],
"details": {
"remoteAddress": "127.0.0.1",
"sessionId": null
},
"authenticated": true,
"principal": {
"password": null,
"username": "test_user",
"authorities": [
{
"authority": "admin"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"credentials": null,
"name": "test_user"
},
"principal": {
"password": null,
"username": "test_user",
"authorities": [
{
"authority": "admin"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"oauth2Request": {
"clientId": "web",
"scope": [],
"requestParameters": {
"grant_type": "custom"
},
"resourceIds": [],
"authorities": [
{
"authority": "ROLE_USER"
}
],
"approved": true,
"refresh": false,
"redirectUri": null,
"responseTypes": [],
"extensions": {},
"grantType": "custom",
"refreshTokenRequest": null
},
"clientOnly": false,
"credentials": "",
"name": "test_user"
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/136865.html