文章目录
- 0、库表准备
- 1、项目结构
- 2、基于数据库的认证
- 3、授权服务器配置
- 4、授权服务器效果测试
- 5、资源服务器配置
- 6、其他授权模式测试
-
- [6.1 密码模式](#6.1 密码模式)
- [6.2 简化模式](#6.2 简化模式)
- [6.3 客户端模式](#6.3 客户端模式)
- [6.4 refresh_token模式](#6.4 refresh_token模式)
- 7、令牌换为jwt格式
相关📕:【Spring Security Oauth2 配置理论部分】
0、库表准备
库表结构:
oauth2的相关表SQL:
java
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
基于RBAC,简化下,只要角色,不要权限表,表结构为:
1)用户表sys_user
2)角色表sys_role
3)用户角色关系表sys_user_role
1、项目结构
创建两个服务,一个充当授权服务器,结构为:
另一个充当资源服务器,结构为:
数据库层采用mysql + mybatis-plus实现,相关依赖:
xml
<dependencies>
<!--spring security starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--spring security oauth核心依赖-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</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>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.4.0</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
application.yml内容:
yaml
# 资源服务器同配置,端口为9010
server:
port: 9009
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test-db?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
username: root
password: root123
main:
allow-bean-definition-overriding: true
logging:
level:
com.itheima: debug
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.plat.domain
2、基于数据库的认证
创建Po:
java
@TableName("sys_user")
@Data
public class SysUserPo implements Serializable {
private Integer id;
private String username;
private String password;
public Integer getId() {
return id;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
java
@TableName("sys_role")
@Data
public class SysRolePo implements GrantedAuthority, Serializable {
private Integer id;
private String roleName;
private String roleDesc;
@Override
public String getAuthority() {
return this.roleName; //注意这里权限的处理,通过实现GrantedAuthority, 和框架接轨
}
}
创建一个中转类,实现UserDetails,以后返回给框架(也可以用框架自己的User类,我觉得自己写个中转类更顺手)。注意其聚合SysUserPo以及权限属性。因SysUser我设计的简略,因此UserDetails的是否被禁用、是否过期等字段直接返回true,不再去自定义的SysUser中去查
java
@Data
@Builder
public class SecurityUser implements UserDetails {
private SysUserPo sysUserPo;
private List<SysRolePo> roles;
public SecurityUser(SysUserPo sysUserPo, List<SysRolePo> roles) {
this.sysUserPo = sysUserPo;
this.roles = roles;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public String getPassword() {
return this.sysUserPo.getPassword();
}
@Override
public String getUsername() {
return this.sysUserPo.getUsername();
}
/**
* 以下字段,我的用户表设计简单,没有过期、禁用等字段
* 这里都返回true
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Mapper:
java
@Repository
@Mapper
public interface UserMapper extends BaseMapper<SysUserPo> {
@Select("select * from sys_user where username = #{username}")
SysUserPo selectUserByName(String username);
}
java
@Repository
@Mapper
public interface RoleMapper extends BaseMapper<SysRolePo> {
@Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc "+
"FROM sys_role r ,sys_user_role ur "+
"WHERE r.id=ur.role_id AND ur.user_id=#{uid}")
public List<SysRolePo> selectAuthByUserId(Integer uid);
}
写UserDetialsService接口的实现类,好自定义用户查询逻辑:
java
public interface UserService extends UserDetailsService {
java
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Resource
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//自定义用户类
SysUserPo sysUserPo = userMapper.selectUserByName(username);
//权限
List<SysRolePo> authList = roleMapper.selectAuthByUserId(sysUserPo.getId());
return new SecurityUser(sysUserPo, authList);
}
}
3、授权服务器配置
注入DataSource对象,定义授权服务器需要的相关Bean:
java
@Configuration
public class OAuth2Bean {
@Resource
private DataSource dataSource; //数据库连接池对象
/**
* 客户端服务详情
* 从数据库查询客户端信息
*/
@Bean(name = "jdbcClientDetailsService")
public JdbcClientDetailsService clientDetailsService(){
return new JdbcClientDetailsService(dataSource);
}
/**
* 授权信息保存策略
*/
@Bean(name = "jdbcApprovalStore")
public ApprovalStore approvalStore(){
return new JdbcApprovalStore(dataSource);
}
/**
* 令牌存储策略
*/
@Bean(name = "jdbcTokenStore")
public TokenStore tokenStore(){
//使用数据库存储令牌
return new JdbcTokenStore(dataSource);
}
//设置授权码模式下,授权码如何存储
@Bean(name = "jdbcAuthorizationCodeServices")
public AuthorizationCodeServices authorizationCodeServices(){
return new JdbcAuthorizationCodeServices(dataSource);
}
}
配置OAuth2的授权服务器:
java
@Configuration
@EnableAuthorizationServer //OAuth2的授权服务器
public class OAuth2ServiceConfig implements AuthorizationServerConfigurer {
@Resource(name = "jdbcTokenStore")
private TokenStore tokenStore; //注入自定义的token存储配置Bean
@Resource(name = "jdbcClientDetailsService")
private ClientDetailsService clientDetailsService; //客户端角色详情
@Resource
private AuthenticationManager authenticationManager; //注入安全配置类中定义的认证管理器Bean
@Resource(name = "jdbcAuthorizationCodeServices")
private AuthorizationCodeServices authorizationCodeServices; //注入自定义的授权码模式服务配置Bean
@Resource(name = "jdbcApprovalStore")
private ApprovalStore approvalStore; //授权信息保存策略
//token令牌管理
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService); //客户端信息服务,即向哪个客户端颁发令牌
tokenServices.setSupportRefreshToken(true); //支持产生刷新令牌
tokenServices.setTokenStore(tokenStore); //令牌的存储策略
tokenServices.setAccessTokenValiditySeconds(7200); //令牌默认有效期2小时
tokenServices.setRefreshTokenValiditySeconds(259200); //refresh_token默认有效期三天
return tokenServices;
}
/**
* token令牌端点访问的安全策略
* (不是所有人都可以来访问框架提供的这些令牌端点的)
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer authorizationServerSecurityConfigurer) throws Exception {
authorizationServerSecurityConfigurer.tokenKeyAccess("permitAll()") //oauth/token_key这个端点(url)是公开的,不用登录可调
.checkTokenAccess("permitAll()") // oauth/check_token这个端点是公开的
.allowFormAuthenticationForClients(); //允许客户端表单认证,申请令牌
}
/**
* Oauth2.0客户端角色的信息来源:内存、数据库
* 这里用数据库
*/
@Override
public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
clientDetailsServiceConfigurer.withClientDetails(clientDetailsService);
}
/**
* 令牌端点访问和令牌服务(令牌怎么生成、怎么存储等)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager) //设置认证管理器,密码模式需要
.authorizationCodeServices(authorizationCodeServices) //授权码模式需要
.approvalStore(approvalStore)
.tokenServices(tokenServices()) //token管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST); //允许Post方式访问
}
}
web安全配置类:
java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserService userService;
//设置权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf()
.disable();
}
//AuthenticationManager对象在Oauth2认证服务中要使用,提取放到IOC容器中
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//指定认证对象的来源
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
授权服务器配置完成,启动服务。
4、授权服务器效果测试
浏览器模拟客户端系统请求资源,客户端系统自已重定向到以下路径:
java
http://localhost:9009/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=https://www.baidu.com
向服务方获取授权码。到达服务方系统的登录页面,输入用户在服务方系统的账户密码:
服务方系统校验通过,询问用户是否向c1客户端系统开放权限all去获取它的资源:
点击同意,重定向到客户端注册的redirect_url,并返回授权码:
客户端系统用授权码去/oauth/token换取令牌:
成功获得令牌。携带此令牌向资源服务器发起请求。
java
ps:复习认证授权的对接流程
- 客户端系统向本地服务发起授权申请
- 客户端系统授权地址重定向到服务端系统的/oauth/authorize接口
- 客户端系统向服务端系统的认证中心发起授权申请
- 服务端系统校验是否已登录
- 未登录则需要在服务端系统页面完成用户登录
- 服务端系统认证中心发放授权码
- 客户端系统申请token
- 客户端系统使用code向服务端换取token
- 服务端系统返回token及有效期
- 服务端系统同步缓存token
- 返回token给客户端系统
5、资源服务器配置
配置一个远程校验token的Bean,设置校验token的端点url,以及资源服务自己的客户端id和密钥:
java
@Configuration
public class BeanConfig {
@Bean
public ResourceServerTokenServices tokenServices() {
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://localhost:9009/oauth/check_token");
services.setClientId("resourceServiceId");
services.setClientSecret("123");
return services;
}
}
配置授权服务器:
java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true)
public class OAuthSourceConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Resource
private DataSource dataSource;
@Resource
ResourceServerTokenServices resourceServerTokenServices;
@Bean
public TokenStore jdbcTokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID) //资源id
.tokenStore(jdbcTokenStore()) //告诉资源服务token在库里
.tokenServices(resourceServerTokenServices)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//这就是给客户端发token时的scope,这里会校验scope标识
.antMatchers("/**").access("#oauth2.hasAnyScope('all')")
.and()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
写个测试接口:
java
@RestController
public class ResourceController {
@GetMapping("/r/r1")
public String r1(){
return "access resource 1";
}
@GetMapping("/r/r2")
public String r2(){
return "access resource 2";
}
}
携带上面申请的令牌访问测试接口。token正确时:
token错误时:
6、其他授权模式测试
上面测完了授权码模式,该模式最安全,因为access_token只在服务端在交换,而不经过浏览器,令牌不容易泄露。
6.1 密码模式
测试密码模式,刚开始报错:unauthorized grant type:password。
想起客户端注册信息是我手动插入到oauth表里的,新改个字段:
一切正常:
很明显,这种模式会把用户在服务端系统的账户和密码泄漏给客户端系统。因此该模式一般用于客户端系统也是自己公司开发的情况。
6.2 简化模式
相比授权码模式,少了一步授权码换token的步骤。
response_type=token,说明是简化模式。
java
/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
简化模式用于客户端只是个前端页面的情况。即没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码+换取token
6.3 客户端模式
使用客户端模式:
java
/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
参数:
- client_id:客户端准入标识。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写client_credentials表示客户端模式
简单但不安全,需要对客户端系统很信任,可用于合作方系统间对接:
6.4 refresh_token模式
7、令牌换为jwt格式
以上Demo,资源服务校验令牌的合法性得通过RemoteTokenServices来调用授权服务的/oauth/check_token接口。如此,会影响系统的性能。 ⇒ 引入JWT 。让资源服务不再需要远程调用授权服务来校验令牌,而是让资源服务本身就可以校验。相关依赖:
xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
授权服务改动:设置对称密钥,令牌存储策略TokenStore改为JwtTokenStore
java
@Configuration
public class OAuth2Bean {
@Value("${jwt.secret:oauth9527}")
private String SIGNING_KEY;
@Resource
private DataSource dataSource; //数据库连接池对象
@Resource(name = "bCryptPasswordEncoder")
private PasswordEncoder passwordEncoder;
/**
* 令牌存储策略
*/
@Bean(name = "jwtTokenStore")
public TokenStore tokenStore(){
//JWT
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
/**
* 客户端服务详情
* 从数据库查询客户端信息
*/
@Bean(name = "jdbcClientDetailsService")
public JdbcClientDetailsService clientDetailsService(){
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
/**
* 授权信息保存策略
*/
@Bean(name = "jdbcApprovalStore")
public ApprovalStore approvalStore(){
return new JdbcApprovalStore(dataSource);
}
//设置授权码模式下,授权码如何存储
@Bean(name = "jdbcAuthorizationCodeServices")
public AuthorizationCodeServices authorizationCodeServices(){
return new JdbcAuthorizationCodeServices(dataSource);
}
}
TokenService新增后面的令牌增强:
java
//token令牌管理
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService); //客户端信息服务,即向哪个客户端颁发令牌
tokenServices.setSupportRefreshToken(true); //支持产生刷新令牌
tokenServices.setTokenStore(tokenStore); //令牌的存储策略
tokenServices.setAccessTokenValiditySeconds(7200); //令牌默认有效期2小时
tokenServices.setRefreshTokenValiditySeconds(259200); //refresh_token默认有效期三天
//令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
tokenServices.setTokenEnhancer(tokenEnhancerChain);
return tokenServices;
}
资源服务器上,使用同一个对称密钥以及JwtTokenStore:
java
@Configuration
public class BeanConfig {
@Value("${jwt.secret:oauth9527}")
private String SIGNING_KEY;
/**
* 令牌存储策略
*/
@Bean(name = "jwtTokenStore")
public TokenStore tokenStore(){
//JWT
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
//@Bean //不再需要这个远程校验token的Bean了
public ResourceServerTokenServices tokenServices() {
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://localhost:9009/oauth/check_token");
services.setClientId("resourceServiceId");
services.setClientSecret("123");
return services;
}
}
资源服务器配置类中,不再需要远程校验的RemoteTokenServices
验证下效果:
携带jwt的token访问资源服务: