SpringSecurity-OAuth2万文详解
Posted springboot葵花宝典
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity-OAuth2万文详解相关的知识,希望对你有一定的参考价值。
SpringSecurity-OAuth2万文详解
Oauth2.0是目前流行的授权机制,用于授权第三方应用,获取数据。Oauth协议为用户资源的授权提供一个安全、开放并且简易的规范标准。和以往授权不同的是Oauth不会使第三方触及到用户的账号信息(用户和密码),也就是说第三方不需要使用用户的用户名和密码就可以获取到该用户的用户资源权限。
OAuth2设计的角色
- 资源所有者(Resource Owner):通常是用户(User),如昵称、头像这些资源的拥有者(用户只是将这些资源放到服务提供商的资源服务器中)。
- 第三方应用:或者称为第三方客户端(Clinet),希望使用资源服务器提供的资源
- 认证服务器(Authorization Server):专门用于对资源所有者的身份进行认证,对要访问的资源进行授权、产生令牌的服务器。访问资源,需要通过认证服务器由资源所有者授权才可以访问。
- 资源服务器(Resource Server):存储用户的资源,验证令牌有效性。比如:微信资源服务器存储了微信用户信息,淘宝资源服务器存储了淘宝的用户信息。
- 服务提供商(Service Provider):认证服务和资源服务归属于一个机构,该机构就是服务提供商。
OAuth2认证流程
OAuth在第三方应用和服务提供商之间,设置一个授权层(authorization layer)。第三方应用不能直接登录"服务提供商",只可以通过授权层将"第三方应用"和用户区分开来。"第三方应用"通过授权层获取令牌(accesstoken),获取令牌后拿令牌去访问服务提供商。令牌和用户密码不同,可以指定授权层令牌的权限范围和有效期,"服务提供商"根据令牌的权限范围和有效期,向"第三方应用"开放用户对应的资源。第三方客户端登录主要步骤如下:
- 第三方应用,向认证服务器请求授权。
- 用户告知认证服务器同意授权(通常是通过用户扫码或输入“服务提供商”的用户名密码的方式)
- 认证服务器向第三方应用告知授权码(code)
- 第三方应用使用授权码(code)申请Access Token
- 认证服务器验证授权码,颁发Access Token
OAuth2四种授权方式
OAuth2有四种授权方式分别如下
授权码模式(Authorization Code)
授权码模式(Authorization Code):功能是最完整的,流程也是最严密的,国内各大服务提供商(微信、微博、淘宝、百度)都是使用此授权模式进行授权。该授权模式可以确定是用户进行授权的,并且令牌是认证服务器放发到第三方应用服务器,而不是浏览器上。
简化模式(Implicit)
简化模式(Implicit):和授权码模式不同的是,令牌发放给浏览器,OAuth2客户端运行在浏览器中,通过KS脚本去申请令牌。而不是发放该第三方应用的服务器。
密码模式(resource owner password credentials)
密码模式(resource owner password credentials):将用户和密码传过去,直接获取accesstokne,用户同意授权动作是在第三方应用上完成,而不是在认证服务器。第三方应用申请令牌时,直接带用户名和密码去向认证服务器申请令牌。这种方式认证服务器无法断定用户是否真的授权,用户和密码可能是第三方应用盗取过来的。
流程如下:
- 用户向客户端直接提供认证服务器想要的用户名和密码。
- 客户端将用户名和密码发给认证服务器,向认证服务器请求令牌
- 认证服务器确认后,向客户端提供访问令牌
客户端模式(client credentials)
客户端模式(client credentials):使用较少,当一个第三方应用自己本身需要获取资源(而不是以用户的名义),而不是获取用户资源时,客户端模式十分有用。
具体流程如下:
- 客户端向认证服务器进行身份认证,并要求一个访问令牌
- 认证服务器确认后,向客户端提供访问令牌
Spring Security OAuth2认证服务器
Spring Security登录信息存储在Session中,每次访问服务的时候,都会查看浏览器中Cookie中是不是存在JSESSIONID,如果不存在JSESSIONID会新建一个Session,将新建的SessionID保存到Cookie中。每一次发送请求都会通过浏览器的SessionID查找到对应的Session对象。从而获取用户信息。
前后端分离后,前端部署在单独的Web服务器,后端部署在另外的应用服务器上,浏览器先访问Web服务器,Web服务器访问请求到应用服务器,这样使用Cookie存储就不合适具体原因如下:
- 开发复杂
- 安全性差
- 客户体验差
- 有些前端技术不支持Cookie,比如:小程序
解决方式:
使用令牌方式进行认证解决上面说的问题,可以使用OAuth2协议。
基础模块创建
- 创建spring-oauth2-base模块
- 在spring-oauth2-base模块的pom.xml中添加相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 在spring.oauth2.base.api模块下添加IResultCode接口
/**
* 统一返回结果接口
*/
public interface IResultCode
/**
* 返回码
*
* @return int
*/
int getCode();
/**
* 返回消息
*
* @return String
*/
String getMsg();
- 在spring.oauth2.base.api模块下添加ResultCode实现IResultCode
@Getter
@AllArgsConstructor
public enum ResultCode implements IResultCode
/**
* 操作成功
*/
SUCCESS(200, "操作成功"),
/**
* 业务异常
*/
FAILURE(400, "业务异常"),
/**
* 服务异常
*/
ERROR(500, "服务异常"),
/**
* 参数错误
*/
GLOBAL_PARAM_ERROR(540, "参数错误");
/**
* 状态码
*/
final int code;
/**
* 消息内容
*/
final String msg;
- 在spring.oauth2.base.api模块下添加Result用于统一结果处理
@Data
@Getter
public class Result<T> implements Serializable
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
private int code;
/**
* 状态信息
*/
private String msg;
/**
*
*/
private Date time;
private T data;
private Result()
this.time = new Date();
private Result(IResultCode resultCode)
this(resultCode, null, resultCode.getMsg());
private Result(IResultCode resultCode, String msg)
this(resultCode, null, msg);
private Result(IResultCode resultCode, T data)
this(resultCode, data, resultCode.getMsg());
private Result(IResultCode resultCode, T data, String msg)
this(resultCode.getCode(), data, msg);
private Result(int code, T data, String msg)
this.code = code;
this.data = data;
this.msg = msg;
this.time = new Date();
/**
* 返回状态码
*
* @param resultCode 状态码
* @param <T> 泛型标识
* @return ApiResult
*/
public static <T> Result<T> success(IResultCode resultCode)
return new Result<>(resultCode);
public static <T> Result<T> success(String msg)
return new Result<>(ResultCode.SUCCESS, msg);
public static <T> Result<T> success(IResultCode resultCode, String msg)
return new Result<>(resultCode, msg);
public static <T> Result<T> data(T data)
return data(data, "处理成功");
public static <T> Result<T> data(T data, String msg)
return data(ResultCode.SUCCESS.code, data, msg);
public static <T> Result<T> data(int code, T data, String msg)
return new Result<>(code, data, data == null ? "承载数据为空" : msg);
public static <T> Result<T> fail()
return new Result<>(ResultCode.FAILURE, ResultCode.FAILURE.getMsg());
public static <T> Result<T> fail(String msg)
return new Result<>(ResultCode.FAILURE, msg);
public static <T> Result<T> fail(int code, String msg)
return new Result<>(code, null, msg);
public static <T> Result<T> fail(IResultCode resultCode)
return new Result<>(resultCode);
public static <T> Result<T> fail(IResultCode resultCode, String msg)
return new Result<>(resultCode, msg);
public static <T> Result<T> condition(boolean flag)
return flag ? success("处理成功") : fail("处理失败");
认证服务器模块创建
- 创建spring-oauth2-server模块
- 添加依赖pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- for OAuth 2.0 -->
<!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 配置application.yml
server:
port: 8899
spring:
thymeleaf:
cache: false
application:
name: oauth2-server
认证服务器配置-授权码模式
创建配置类
创建作用:
- 配置允许访问此认证服务器的客户端信息,没有再次配置的客户端信息不允许访问。
- 管理令牌
- 配置令牌管理策略(JDBC/Redis/JWT)
- 配置令牌生成策略
- 配置令牌端点
- 令牌端点的安全配置
创建认证服务配置类
在spring-oauth2-server
模块创建认证配置类:
- 创建
spring.oauth2.server.config.OAuth2AuthorizationServerConfig
类继承AuthorizationServerConfigurerAdapter
- 在
OAuth2AuthorizationServerConfig
类上添加注解:
-
@Configuration
-
@EnableAuthorizationServer
认证服务器
- 配置说明:
可以配置:"authorization_code", "password", "implicit","client_credentials","refresh_token"
-
scopes
:授权范围标识,比如指定微服务名称,则只可以访问指定的微服务 -
autoApprove
: false跳转到授权页面手动点击授权,true不需要手动授权,直接响应授权码 -
redirectUris
:当获取授权码后,认证服务器会重定向到指定的这个URL
,并且带着一个授权码code
响应。 -
withClient
:允许访问此认证服务器的客户端ID -
secret
:客户端密码,加密存储 -
authorizedGrantTypes
:授权类型,支持同时多种授权类型
/**
* 认证服务器
*/
@Configuration
@EnableAuthorizationServer //开启认证服务器
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter
//在 MyOAuth2Config 添加到容器了
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 配置被允许访问此认证服务器的客户端详细信息
* 1.内存管理
* 2.数据库管理方式
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception
clients.inMemory()
//客户端名称
.withClient("test-pc")
//客户端密码
.secret(passwordEncoder.encode("123456"))
//资源id,商品资源
.resourceIds("oauth2-server")
//授权类型, 可同时支持多种授权类型
.authorizedGrantTypes("authorization_code", "password", "implicit","client_credentials","refresh_token")
//授权范围标识,哪部分资源可访问(all是标识,不是代表所有)
.scopes("all")
// false 跳转到授权页面手动点击授权,true 不用手动授权,直接响应授权码
.autoApprove(false)
.redirectUris("http://www.baidu.com/")//客户端回调地址
;
统一管理Bean配置类
创建spring.oauth2.server.config.MyOAuth2Config
类,向容器中添加加密方式 BCrypt
@Configuration
public class MyOAuth2Config
@Bean
public PasswordEncoder passwordEncoder()
return new BCryptPasswordEncoder() ;
创建安全配置类
指定认证用户的用户名和密码,用户和密码是资源的所有者。这个用户名和密码和客户端id和密码是不一样的,客户端ID和密码是应用系统的标识,每个应用系统对应一个客户端id和密码。
在spring-oauth2-server
模块创建安全配置类:
- 创建
spring.oauth2.server.config.OAuth2SecurityConfig
类继承WebSecurityConfigurerAdapter
- 在
OAuth2SecurityConfig
类上添加注解
-
@EnableWebSecurity
,包含了@Confifiguration
注解
/**
* 安全配置类
*/
@EnableWebSecurity
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService myUserDetailsService;
/**
* 用户类信息
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder.encode("123456"))
.authorities("admin_role")
;
令牌访问端点
Spring Security对OAuth2提供了默认可访问端点,即URL
-
/oauth/authorize
:申请授权码code,涉及类AuthorizationEndpoint
-
/oauth/token
:获取令牌token,涉及类TokenEndpoint
-
/oauth/check_token
:用于资源服务器请求端点来检查令牌是否有效,涉及类CheckTokenEndpoint
-
/oauth/confirm_access
:用于确认授权提交,涉及类WhitelabelApprovalEndpoint
-
/oauth/error
:授权错误信息,涉及WhitelabelErrorEndpoint
-
/oauth/token_key
:提供公有密匙的端点,使用JWT令牌时会使用,涉及类TokenKeyEndpoint
获取请求授权码Code
涉及类org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint
- 使用以下地址申请授权码
http://localhost:8899/oauth/authorize?client_id=test-pc&response_type=code
- 当请求到达授权中心
AuthorizationEndpoint
后,授权中心会要求资源所有者进行身份验证
注:
1. 此处输入的用户名、密码是在认证服务器输入的(看端口8899),而不是在客户端上输入的,这样更加安全,因为客户端不知道用户名和密码
2. 密码模式中,输入的用户名、密码不是在认证服务器上输入,而是在客户端输入的,这样客户端就不太安全。
- 点击登录以后,会跳转到指定的
redirect_uri
,回调路径会携带一个授权码(code=8OsDS8
),如下图
- 获取到授权码(code) 后,就可以通过它来获取访问令牌(access_token)。
通过授权码获取令牌token
涉及:TokenEndpoint
POST
方式请求:http://localhost:8899/oauth/token
- Postman中将 client_id:client_secret 通过 Base64 编码
-
post
方式,请求体中指定授权方式和授权码
- 每个授权码申请令牌后就会失效,需要重新发送请求获取授权码再去认证,不然就会请求认证失败
认证服务器配置-密码模式
密码模式(resource owner password credentials),用户向客户端提供自己在认证服务器上的用户和密码,然后客户端通过用户提供的用户名和密码向认证服务器获取令牌。
但是如果用户名和密码遗漏,认证服务器无法判断客户端提交的用户和密码是否是盗取的,那意味着令牌就可以随时获取,信息容易泄露。
配置密码模式
- 在安全配置类中
spring.oauth2.server.config.OAuth2SecurityConfig
,将AuthenticationManager
注入到bean
/**
* password 密码模式要使用此认证管理器
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
指定密码模式
在认证服务器配置类OAuth2AuthorizationServerConfig
中
- 覆盖父类的
configure(AuthorizationServerEndpointsConfigurer endpoints)
方法,用于配置令牌访问端点,把authenticationManager
注入并添加
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
//密码模式需要配置认证管理器
endpoints.authenticationManager(authenticationManager);
- 针对
test-pc
客户端添加支持密码模,可以同时支持多个模式,配置如下
.authorizedGrantTypes("authorization_code", "password", "implicit","client_credentials","refresh_token")
获取令牌token
使用浏览器访问http://localhost:8899/oauth/token
- 重新启动浏览器
- Postman中将 client_id:client_secret 通过 Base64 编码
- post 方式,请求体中指定: 授权方式 、用户名、密码
认证服务器配置- 简化授权模式
不通过第三方应用程序,直接在浏览器中向认证服务器申请令牌,不需要先获取授权码。直接可以一次请求就可得到令牌,在 redirect_uri 指定的回调地址中传递令牌( access_token )。该模式适合直接运行在浏览器上的应用,不用后端支持(例如 javascript 应用)
简化模式
在OAuth2AuthorizationServerConfig
类中的configure(ClientDetailsServiceConfigurer clients)
方法中指定implicit
获取令牌token
- 打开浏览器,输入访问地址
http://localhost:8899/oauth/authorize?client_id=test-pc&response_type=token
注:此时response_typ
的参数值必须是token
- 当请求到达认证服务器的 AuthorizationEndpoint 后,它会要求资源所有者做身份验证 :
- 点击登录以后,会跳转到指定的
redirect_uri
,回调路径会,回调路径携带着令牌 access_token 、 expires_in 、 scope 等 ,如下图:
认证服务器配置- 客户端授权模式
客户端模式(client credentials)是指客户端以自己名义,而不是用户名义,向认证服务器进行认证,严格说客户端模式并不属于
OAuth2
框架所解决的问题,在这种模式下,用户直接向客户端注册,客户端以自己的名义向认证服务器提供服务,实际上并不存在授权问题。
- 客户端向认证服务器进行身份认证,并要求一个访问令牌。
- 认证服务器确认无误后,向客户端提供访问令牌。
指定客户端模式
在OAuth2AuthorizationServerConfig
类的configure(ClientDetailsServiceConfigurer clients)
方法中指定client_credentials
获取令牌token
- Postman中将 client_id:client_secret 通过 Base64 编码
2.post 方式,请求体中指定授权类型grant_type :client_credentials
注响应结果没有刷新令牌
认证服务器配置-令牌刷新策略
如果用户访问资源的时候,客户端的令牌已经过期,那么就需要更新令牌,申请一个新的访问令牌。
客户端发出更新令牌的Http请求,包含以下参数:
-
grant_type
:表示使用授权模式,此处固定值为refresh_token
-
refresh_token
:表示早前收到的需要更新的令牌-
scope
:表示申请的授权范围,不可以超出上一次申请的范围。
**注:刷新令牌只有在授权模式和密码模式中才有,对应的指定这两种模式时,在类型上加上refresh_token
**。
获取新令牌报错
- Postman中将 client_id:client_secret 通过 Base64 编码
- post 方式,请求体中指定:授权类型、刷新令牌
当前报错: Internal Server Error , 对应idea控制台也发出警告: UserDetailsService is required.
原因当前需要使用内存方式存储了用户令牌,应用使用UserDetailsService才行
解决办法
创建UserDetailsService实现
- 创建
MyUserDetailsService
动态获取用户令牌
@Component
public class MyUserDetailsService implements UserDetailsService
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
return new User("admin", passwordEncoder.encode("123456"),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin_role"));
- 在安全配置类
OAuth2SecurityConfig
中注入myUserDetailsService
@Autowired
private UserDetailsService myUserDetailsService;
/**
* 用户类信息
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
auth.userDetailsService(myUserDetailsService) ;
3.认证配置类OAuth2AuthorizationServerConfig
的configure(AuthorizationServerEndpointsConfigurer endpoints)
方法上加入到令牌端点上
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
//密码模式需要配置认证管理器
endpoints.authenticationManager(authenticationManager);
//刷新令牌获取新令牌时需要
endpoints.userDetailsService(myUserDetailsService);
测试获取新令牌
- 重启认证服务器
- Postman中将 client_id:client_secret 通过 Base64 编码
- post 方式,请求体中指定:授权类型 refresh_token 、刷新令牌值
认证服务器配置-令牌管理策略Redis & JDBC
默认情况下,令牌是通过randomUUID产生的32为随机数来进行填充,从而产生的令牌默认是存储在内存中。
- 内存存储采用的是
TokenStore
接口默认实现类Chrome Network面板工具之万文多图详解
阿尔茨海默综合症预测前三名方案总结与2022年最新方案分享(万文详解)
工程机械核心部件寿命预测前三名方案总结与2022年最新方案分享(万文详解)
万文多图详解新学Python之Jupyter Notebook学习(持续更新)
[JavaWeb]SpringSecurity-OAuth2.0 统一认证资源分离的配置,用于分布式架构模块化开发的认证体系