OAuth 协议的授权模式共分为 4 种,分别说明如下:
授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本 都是使用这种模式。
简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。
密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
由于我主要制作的项目都是基于web的而且是自己使用所以目前只搭建了:密码模式,客户端模式,验证码模式(自己定义的)
以下代码省略UserDetails和UserDetailsService相关实现
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.security.oauthgroupId>
<artifactId>spring-security-oauth2artifactId>
<version>2.3.3.RELEASEversion>
dependency>
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
INSERT INTO `数据库名`.`oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('client_1', 'order', '$2a$10$n9a19dUkOx/UzGWPJ5PWHukPuNxNd2JfwN9ibi7yfwUIKQXBwuVca', 'select', 'client_credentials,refresh_token', NULL, 'oauth2', NULL, NULL, NULL, NULL);
INSERT INTO `数据库名`.`oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('client_2', 'order', '$2a$10$n9a19dUkOx/UzGWPJ5PWHukPuNxNd2JfwN9ibi7yfwUIKQXBwuVca', 'select', 'password,refresh_token,captcha', NULL, 'oauth2', NULL, NULL, NULL, NULL);
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static final String RESOURCE_IDS = "order";
@Autowired
DataSource dataSource;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
UserDetailsService userDetailsService;
@Autowired
RedisTemplate redisTemplate;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// String finalSecret = "{bcrypt}" + new BCryptPasswordEncoder().encode("123456");
// System.out.println(finalSecret);
// //配置两个客户端,一个用于password认证一个用于client认证
// clients.inMemory()
//
// //client模式
// .withClient("client_1")
// .resourceIds(RESOURCE_IDS)
// .authorizedGrantTypes("client_credentials", "refresh_token")
// .scopes("select")
// .authorities("oauth2")
// .secret(finalSecret)
//
// .and()
//
// //密码模式
// .withClient("client_2")
// .resourceIds(RESOURCE_IDS)
// .authorizedGrantTypes("password", "refresh_token")
// .scopes("select")
// .authorities("oauth2")
// .secret(finalSecret);
//这个地方指的是从jdbc查出数据来存储
clients.withClientDetails(clientDetails());
}
/**
* 从数据库读取客户端配置
*/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 认证服务端点配置
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
//获取grant_type类型组合
TokenGranter tokenGranter = TokenGranterExt.getTokenGranter(authenticationManager, endpoints, redisTemplate);
endpoints
//设置grant_type类型集合
.tokenGranter(tokenGranter)
//用户管理
.userDetailsService(userDetailsService)
//token存到redis
.tokenStore(tokenStore())
//启用oauth2管理
.authenticationManager(authenticationManager)
//接收GET和POST
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
//允许表单认证
.allowFormAuthenticationForClients()
//.passwordEncoder(passwordEncoder)
//允许 check_token 访问
.checkTokenAccess("permitAll()");
}
/**
* 令牌设置,并存入redis
*/
@Bean
public TokenStore tokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
}
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_IDS = "order";
@Autowired
private CustomAuthExceptionHandler customAuthExceptionHandler;
@Autowired
UserLogoutSuccessHandler userLogoutSuccessHandler;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_IDS)
//此处是关键,默认stateless=true,只支持access_token形式,
// OAuth2客户端连接需要使用session,所以需要设置成false以支持session授权
.stateless(false)
.accessDeniedHandler(customAuthExceptionHandler)
.authenticationEntryPoint(customAuthExceptionHandler);
}
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
//解决springSecurty使用X-Frame-Options防止网页被Frame
httpSecurity.headers().frameOptions().disable()
.and()
.logout()
.logoutSuccessHandler(userLogoutSuccessHandler);
httpSecurity
//.addFilterBefore(loginAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
//需要的时候创建session,支持从session中获取认证信息,ResourceServerConfiguration中
//session创建策略是stateless不使用,这里其覆盖配置可创建session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().anyRequest()
.and()
.anonymous()
.and()
.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()//将PreflightRequest不做拦截。
.and()
.authorizeRequests()
.antMatchers(
"/webjars/**",
"/swagger/**",
"/v2/api-docs",
"/doc.html",
"/swagger-ui.html",
"/swagger-resources/**",
"/druid/**",
"/open/**").permitAll()
.and()
.authorizeRequests()
.antMatchers("/**").authenticated();//配置所有访问控制,必须认证过后才可以访问
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 注入AuthenticationManager接口,启用OAuth2密码模式
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
/**
* 通过HttpSecurity实现Security的自定义过滤配置
*
* @param httpSecurity
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll();
}
}
@Configuration
public class GlobalCorsConfiguration {
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
public class CaptchaTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE="captcha";
private final AuthenticationManager authenticationManager;
private RedisTemplate redisTemplate;
public CaptchaTokenGranter(AuthenticationManager authenticationManager,
RedisTemplate redisTemplate,
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
this.redisTemplate=redisTemplate;
}
protected CaptchaTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory,
String grantType) {
//调用父类 接管GRANT_TYPE类型
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager=authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
String username = parameters.get("username");
String password = parameters.get("password");
String captcha=parameters.get("captcha");
String sessionUUID=parameters.get("sessionUUID");
System.out.println(captcha+" "+sessionUUID);
if (StringUtils.isEmpty(captcha)){
throw new InvalidGrantException("验证码不能为空!");
}
//正式环境放行
Object o = redisTemplate.opsForValue().get(sessionUUID);
if (o==null){
throw new InvalidGrantException("验证码不存在!");
}
if (!o.toString().equals(captcha)){
throw new InvalidGrantException("验证码错误!");
}
// Protect from downstream leaks of password
parameters.remove("password");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
}
catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
}
catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
/**
* TokenGranter 扩展 将自定义的grant_type类型添加到oauth2中
* 使用方法:
* 在configure(AuthorizationServerEndpointsConfigurer endpoints)中:
* //获取自定义tokenGranter
* TokenGranter tokenGranter = TokenGranterExt.getTokenGranter(authenticationManager, endpoints, baseRedis, userClient, socialProperties);
* endpoints.tokenGranter(tokenGranter);
*
*/
public class TokenGranterExt {
public static TokenGranter getTokenGranter(final AuthenticationManager authenticationManager,
final AuthorizationServerEndpointsConfigurer endpointsConfigurer,
RedisTemplate redisTemplate
) {
// 默认tokenGranter集合 security 自带的
List<TokenGranter> granters = new ArrayList<>(Collections.singletonList(endpointsConfigurer.getTokenGranter()));
//添加验证码
granters.add(new CaptchaTokenGranter(authenticationManager, redisTemplate, endpointsConfigurer.getTokenServices(), endpointsConfigurer.getClientDetailsService(), endpointsConfigurer.getOAuth2RequestFactory()));
return new CompositeTokenGranter(granters);
}
}