相关📕:【Spring Security Oauth2 配置理论部分】
库表结构:

oauth2的相关表SQL:
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

创建两个服务,一个充当授权服务器,结构为:

另一个充当资源服务器,结构为:

数据库层采用mysql + mybatis-plus实现,相关依赖:
org.springframework.boot spring-boot-starter-security org.springframework.security.oauth spring-security-oauth2 2.3.4.RELEASE org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java 8.0.30 com.baomidou mybatis-plus-boot-starter 3.4.0 com.baomidou mybatis-plus 3.4.0 org.projectlombok lombok application.yml内容:
# 资源服务器同配置,端口为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 创建Po:
@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; } } @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中去查
@Data @Builder public class SecurityUser implements UserDetails { private SysUserPo sysUserPo; private List roles; public SecurityUser(SysUserPo sysUserPo, List 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:
@Repository @Mapper public interface UserMapper extends BaseMapper { @Select("select * from sys_user where username = #{username}") SysUserPo selectUserByName(String username); } @Repository @Mapper public interface RoleMapper extends BaseMapper { @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 selectAuthByUserId(Integer uid); } 写UserDetialsService接口的实现类,好自定义用户查询逻辑:
public interface UserService extends UserDetailsService { @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 authList = roleMapper.selectAuthByUserId(sysUserPo.getId()); return new SecurityUser(sysUserPo, authList); } } 注入DataSource对象,定义授权服务器需要的相关Bean:
@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的授权服务器:
@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安全配置类:
@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(); } } 授权服务器配置完成,启动服务。
浏览器模拟客户端系统请求资源,客户端系统自已重定向到以下路径:
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换取令牌:

成功获得令牌。携带此令牌向资源服务器发起请求。
ps:复习认证授权的对接流程 
配置一个远程校验token的Bean,设置校验token的端点url,以及资源服务自己的客户端id和密钥:
@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; } } 配置授权服务器:
@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); } } 写个测试接口:
@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错误时:

上面测完了授权码模式,该模式最安全,因为access_token只在服务端在交换,而不经过浏览器,令牌不容易泄露。
测试密码模式,刚开始报错:unauthorized grant type:password。

想起客户端注册信息是我手动插入到oauth表里的,新改个字段:

一切正常:

很明显,这种模式会把用户在服务端系统的账户和密码泄漏给客户端系统。因此该模式一般用于客户端系统也是自己公司开发的情况。
相比授权码模式,少了一步授权码换token的步骤。

response_type=token,说明是简化模式。
/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com 
简化模式用于客户端只是个前端页面的情况。即没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码+换取token
使用客户端模式:

/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials 参数: - client_id:客户端准入标识。 - client_secret:客户端秘钥。 - grant_type:授权类型,填写client_credentials表示客户端模式 简单但不安全,需要对客户端系统很信任,可用于合作方系统间对接:


以上Demo,资源服务校验令牌的合法性得通过RemoteTokenServices来调用授权服务的/oauth/check_token接口。如此,会影响系统的性能。 ⇒ 引入JWT 。让资源服务不再需要远程调用授权服务来校验令牌,而是让资源服务本身就可以校验。相关依赖:
org.springframework.security spring-security-jwt 1.0.9.RELEASE 授权服务改动:设置对称密钥,令牌存储策略TokenStore改为JwtTokenStore
@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新增后面的令牌增强:
//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:
@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访问资源服务:
