oauth2.0授权模式以及springboot集成实例
最近参与的项目中,使用springboot集成oauth2.0实现账户密码校验登录授权功能,以及在功能中实现数据库账户密码校验、域控用户密码校验以及Redis缓存令牌信息等等功能。结合项目实际使用情况以及相关前辈文献资料,本文初步整理汇总记录Oauth2.0相关知识以及给出初步使用示例,以便后续深入学习以及为后来者提供相关参考;文中不免疏漏之处,请读者不吝指教,感激之至!
1.oauth2.0 介绍
OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0,即完全废止了OAuth1.0。
授权服务器 Authorization Server
资源服务器 Resource Server
授权码模式(authorization code)
隐式模式(implicit)
密码模式(resource owner password credentials)
客户端模式(client credentials)
2.oauth2.0 四种模式
2.1 授权码授权模式
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
最常用的流程,安全性也最高,适用于有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
(1)触发访问鉴权服务
https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求,redirect_uri参数是 B 接受或拒绝请求后的跳转网址,scope参数表示要求的授权范围(这里是只读)
(2)鉴权服务界面授权,触发回调传递授权码code值
https://a.com/callback?code=AUTHORIZATION_CODE
(3)发起请求获取token
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL
client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址。
(4)返回token令牌
{
access_token: "e2721525-c1fd-41fc-96c4-ecc419b41ab6",
expires_in: 7199,
refresh_token: "c52dc91b-5068-44bf-9c0a-cfd7da9a2745",
scope: "scope",
token_type: "bearer"
}
2.2 隐式授权模式
纯前端应用,没有后端,必须将令牌储存在前端,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为"隐式授权"(implicit)
(1)触发访问授权界面
https://b.com/oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
response_type参数为token,表示要求直接返回令牌。
(2)回调 redirect_uri返回token
https://a.com/callback#token=ACCESS_TOKEN
2.3 密码模式
如果高度信任某个应用,也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)
(1)用户名+密码访问鉴权服务获取token
https://oauth.b.com/token?
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
grant_type参数是授权方式,这里的password表示"密码式",username和password分别是用户名和密码,请求通过直接返回token信息。
2.4 客户端凭证模式
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,应用程序想要以自己的名义与授权服务器以及资源服务器进行互动,即有可能多个用户通过应用共享同一个令牌。
(1)访问鉴权服务
https://oauth.b.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
grant_type参数等于client_credentials表示采用凭证式,client_id和client_secret用来让鉴权服务确认应用服务的身份。
3.使用令牌与更新令牌
3.1 使用令牌
请求头添加
Authorization: Bearer ACCESS_TOKEN
3.2 更新令牌
颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
https://b.com/oauth/token?
grant_type=refresh_token&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN
grant_type参数为refresh_token表示要求更新令牌,client_id参数和client_secret参数用于确认身份,refresh_token参数就是用于更新令牌的令牌。
4.oauth2.0 使用实例
springboot集成oauth2.0以及使用Redis做缓存使用示例,当前参与项目中实现账户密码校验登录以及根据j角色限制访问uri功能,因此示例中采用密码模式
(0)引入jar
compile 'org.springframework.cloud:spring-cloud-starter-security'
compile 'org.springframework.cloud:spring-cloud-starter-oauth2'
(1)认证授权服务
@Configuration
@EnableAuthorizationServer
public class LocalAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static final String REDIS_TOKEN_PREFIX = "LOCAL:OAUTH2:";
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Autowired
private ClientDetailsService clientDetailsService;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenServices(tokenServices()).authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//此处采用密码模式 password, 授权码模式 authorization_code,隐式授权模式 implicit,客户端凭证模式 client_credentials
clients.inMemory()
.withClient("client_id")
.secret("client_secret")
.authorizedGrantTypes("password","refresh_token")
.scopes("scope");
}
@Bean
public TokenStore tokenStore(){
RedisTokenStore redisTokenStore = new RedisTokenStore(redisTemplate.getConnectionFactory());
redisTokenStore.setPrefix(REDIS_TOKEN_PREFIX);
return redisTokenStore;
}
@Primary
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
// token 过期时间 2小时
defaultTokenServices.setAccessTokenValiditySeconds(7200);
// refresh_token 过期时间 2天
defaultTokenServices.setRefreshTokenValiditySeconds(172800);
defaultTokenServices.setReuseRefreshToken(true);
return defaultTokenServices;
}
}
(2)web安全配置,账户密码
@Configuration
@EnableWebSecurity
public class LocalSecurityWebAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private LocalUserService userService;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
}
}
/**
* 数据库用户账户密码角色信息
*/
@Service("userDetailsService")
public class LocalUserService implements UserDetailsService {
/**
* 根据登陆用户名称获取用户角色信息
* @param username
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
// 数据库查询获取账户username对应的密码password和角色名称列表
List<String> roles = new ArrayList<String>();
String pwd = "";
for(String role: roles){
grantedAuthorities.add(new SimpleGrantedAuthority(role));
}
return new User(username,new BCryptPasswordEncoder().encode(pwd),grantedAuthorities);
}
}
(2)配置资源服务
@Configuration
@EnableResourceServer
public class LocalResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// 此处可以通过配置指定,哪些角色可以访问哪些uri
// 放开部分api接口不需要进行权限校验处理
http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS,"/**").permitAll();
http.authorizeRequests()
.antMatchers("/oauth/token","/oauth/check_token").permitAll();
// uri路径匹配规则:
//1.首先放置 permitAll规则
//2.其次放置 精确规则 完整路径规则
//3.最后放置 正则匹配规则,范围小的在前,范围大的在后
//4.规则依次匹配,匹配到后不再往下匹配,故用于最大权限的角色需要所有规则都赋予
// http.authorizeRequests().antMatchers(uri1).permitAll();
// http.authorizeRequests().antMatchers(uri2).hasAnyAuthority("role1","role2");
http.authorizeRequests().anyRequest().authenticated();
}
private static class Oauth2RequestMatcher implements RequestMatcher{
// 请求头 Authorization 有Bearer token或者请求参数中包含access_token才进行校验
@Override
public boolean matches(HttpServletRequest request) {
String auth = request.getHeader("Authorization");
boolean haveOauth2Token = (auth!=null) && auth.startsWith("Bearer");
boolean haveAccessToken = request.getParameter("access_token")!=null;
return haveOauth2Token || haveAccessToken;
}
}
@Bean
HttpSessionEventPublisher httpSessionEventPublisher(){
return new HttpSessionEventPublisher();
}
@Bean
public SessionRegistry sessionRegistry(){
SessionRegistry sessionRegistry = new SessionRegistryImpl();
return sessionRegistry;
}
}
5.参考资料
[1] https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
[2] https://www.cnblogs.com/sky-chen/archive/2019/03/13/10523882.html#autoid-1-1-1-0-0-0