什么是 oAuth
oAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 oAuth 是安全的。
什么是 Spring Security
Spring Security 是一个安全框架,前身是 Acegi Security,能够为 Spring 企业应用系统提供声明式的安全访问控制。Spring Security 基于 Servlet 过滤器、IOC 和 AOP,为 Web 请求和方法调用提供身份确认和授权处理,避免了代码耦合,减少了大量重复代码工作。
客户端授权模式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。oAuth 2.0 定义了四种授权方式。
- implicit:简化模式,不推荐使用
- authorization code:授权码模式(本文章采用这种模式)
- resource owner password credentials:密码模式
- client credentials:客户端模式
个人对oAuth2认证过程的理解
编码部分
- 创建项目工程,本文章采用多module方式
主要是配置一个pom,文章结束后代码会发到github,这里不复制了 - 创建统一依赖
统一依赖中配置org.springframework.cloud
springboot2.1.x需要使用Greenwich版本 - 创建认证服务器模块(内存模拟版本)
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// 注入 WebSecurityConfiguration 中配置的 BCryptPasswordEncoder
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置客户端
clients
// 使用内存设置
.inMemory()
// client_id
.withClient("client")
// client_secret
.secret(passwordEncoder.encode("secret"))
// 授权类型
.authorizedGrantTypes("authorization_code")
// 授权范围
.scopes("app")
// 注册回调地址
.redirectUris("http://blog.yhhu.xyz");
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 设置默认的加密方式
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 在内存中创建用户并为密码加密
.withUser("user").password(passwordEncoder().encode("123456")).roles("USER")
.and()
.withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN");
}
}
spring:
application:
name: oauth2-server
server:
port: 8080
==必须遵循这样的请求url==
获取code:在浏览器直接访问http://localhost:8080/oauth/authorize?client_id=client&response_type=code
通过code获取access_token:http://client:secret@localhost:8080/oauth/token
- 创建认证服务器模块(JDBC版本)
- 创建一个oauth数据库,官方给的表示H2的,mysql有点改动,百度找了一个
CREATE TABLE `clientdetails` ( `appId` varchar(128) NOT NULL, `resourceIds` varchar(256) DEFAULT NULL, `appSecret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `grantTypes` varchar(256) DEFAULT NULL, `redirectUrl` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additionalInformation` varchar(4096) DEFAULT NULL, `autoApproveScopes` varchar(256) DEFAULT NULL, PRIMARY KEY (`appId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_access_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication_id` varchar(128) NOT NULL, `user_name` varchar(256) DEFAULT NULL, `client_id` varchar(256) DEFAULT NULL, `authentication` blob, `refresh_token` varchar(256) DEFAULT NULL, PRIMARY KEY (`authentication_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_approvals` ( `userId` varchar(256) DEFAULT NULL, `clientId` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `status` varchar(10) DEFAULT NULL, `expiresAt` timestamp NULL DEFAULT NULL, `lastModifiedAt` timestamp NULL DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_client_details` ( `client_id` varchar(128) NOT NULL, `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `authorized_grant_types` varchar(256) DEFAULT NULL, `web_server_redirect_uri` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_client_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication_id` varchar(128) NOT NULL, `user_name` varchar(256) DEFAULT NULL, `client_id` varchar(256) DEFAULT NULL, PRIMARY KEY (`authentication_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_code` ( `code` varchar(256) DEFAULT NULL, `authentication` blob ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_refresh_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication` blob ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 操作一下oauth_client_details表
需要设置的内容为client_id,client_secret,scope,authorized_grant_types,web_server_redirect_uri,注意client_secret是经过加密的
至此完成验证功能,其他操作和内存版都是一样的,完成后可以在数据库中看到看到token相关信息
RBAC(Role-Based Access Control,基于角色的访问控制)
增加了几个表,模型如下
可以做的简单一点,可是这样做功能更加完备,单单在user表中插入role列的话不便于管理
下面开始具体实现
- 在认证服务器端排除token检查的权限判断
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/oauth/check_token");
}
创建一个service,查询用户使用(代码略)
将授权模式由内存模式改为userDetailService方式
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
// 基于 JDBC 实现,令牌保存到数据
return new JdbcTokenStore(dataSource);
}
@Bean
public ClientDetailsService jdbcClientDetails() {
// 基于 JDBC 实现,需要事先在数据库配置客户端信息
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
{
// 设置令牌
endpoints.tokenStore(tokenStore());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
- 认证中心UserDetail权限判断逻辑
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private TbUserService tbUserService;
@Autowired
private TbPermissionService tbPermissionService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
TbUser tbUser = tbUserService.getByUsername(s);
List<GrantedAuthority> grantedAuthorities= Lists.newArrayList();
if (tbUser != null) {
List<TbPermission> tbPermissions = tbPermissionService.selectByUserId(tbUser.getId());
tbPermissions.forEach(item -> {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(item.getEnname());
grantedAuthorities.add(grantedAuthority);
});
return new User(tbUser.getUsername(),tbUser.getPassword(),grantedAuthorities);
}
return null;
}
}
- 如何自定义一个权限控制方法?(通过permission中url对应的方式来判断权限)
@Component("permission")
public class PermissionCheck {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
boolean hasPermission = false;
//取出当前token对应用户所拥有的权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (antPathMatcher.match(authority.getAuthority(), request.getRequestURI())) {
hasPermission = true;
break;
}
}
return hasPermission;
}
}
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.and()
//管理session的创建策略永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//ALWAYS总是创建HttpSession
//IF_REQUIREDSpring Security只会在需要时创建一个HttpSession
//NEVERSpring Security不会创建HttpSession,但如果它已经存在,将可以使用HttpSession
.and()
.authorizeRequests()
.anyRequest().access("@permission.hasPermission(request,authentication)");
// 以下为配置所需保护的资源路径及权限,需要与认证服务器配置的授权部分对应
// .antMatchers("/**").hasAuthority("SystemContent");
}
}
补充一个错误:Spring security oauth2 throwing "no bean resolver registered" for custom bean
在ResourceServerConfiguration类下补充以下配置
/**
* 以下为采坑地方,如果不使用自己的动态权限控制,下面无需配置也能运行
* 原因暂时不明,解决方案为参考以下issues地址
* https://github.com/spring-projects/spring-security-oauth/issues/730#issuecomment-219480394
*/
@Autowired
private OAuth2WebSecurityExpressionHandler expressionHandler;
@Bean
public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {
OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.expressionHandler(expressionHandler);
}
完整代码地址
github:oauth2