概述
在第一篇中,我们已经讲过了OAuth2单点登录的实际应用场景和技术方案,那么这一篇就具体讲解如何搭建OAuth2的服务。OAuth2只是一个协议,实现该协议的技术产品有很多,比如:微软的ADFS,Oracle的OAM(12c),等等。但这些产品都是大厂研发出来的,基本都是收费的,那么如果我们需要基于开源的技术,自己搭建基于OAuth2的服务该怎么做呢?你可以试试“Spring Cloud全家桶”里面的Spring Security OAuth2。
本文将讲解Spring Security OAuth2的项目实战搭建,由于篇幅有限,文章中只会摘录核心代码,完整代码请上 github地址 查看。
最近看过一个非常复杂Spring Security OAuth2技术架构图,虽然很多功能点我自己也没有用到过,但是这里还是附上吧。
项目搭建
首先是创建一个SpringBoot项目,要在启动类加上 @EnableResourceServer 的注解。
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--oauth2-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.15.RELEASE</version>
</dependency>
<!--freemarker,自定义登录页使用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--jwt,生成jwt token-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
<!-- feign,非必需 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
核心的实现类主要只有两个,一个实现接口 WebSecurityConfigurerAdapter,另一个实现AuthorizationServerConfigurerAdapter接口。
WebSecurityConfigurerAdapter主要用来定义Web请求的路由控制,比如:哪些路由受security控制;自定义登录页;登录成功或失败的处理;注销的处理,等等。包括还有 web.ignoring() 的方法,可以对指定url路径放行,不受单点登录控制。
WebSecurityCA.java
@Configuration
@Order(1)
public class WebSecurityCA extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationFailureHandler appLoginFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login")
.antMatchers("/oauth/authorize")
.antMatchers("/oauth/token")
.antMatchers("/logout")
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
// 自定义登录页面,这里配置了 loginPage, 就会通过 LoginController 的 login 接口加载登录页面
.formLogin()
.loginPage("/login")
.permitAll()
.failureHandler(appLoginFailureHandler)
.failureUrl("/login?error=true")
//注销
.and()
.logout()
.addLogoutHandler(new MyLogoutHandler())
.and()
.csrf().disable();
}
/**
* web ignore比较适合配置前端相关的静态资源,它是完全绕过spring security的所有filter的
* ingore是完全绕过了spring security的所有filter,相当于不走spring security
* permitall没有绕过spring security,其中包含了登录的以及匿名的
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/oauth/logout");
}
/**
* 创建该实例,为了保证 密码模式中可以实现AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
AuthorizationServerConfigurerAdapter接口则是实现OAuth2的核心代码,实现功能包括:开放OAuth2的验证模式;开放的clientId和clientSecret;token按照jwt协议生成;等等。
AuthorizationServerCA.java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerCA extends AuthorizationServerConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired(required = false)
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired(required = false)
private TokenEnhancer jwtTokenEnhancer;
@Autowired
private WebResponseExceptionTranslator customWebResponseExceptionTranslator;
@Autowired
private OAuth2Properties oAuth2Properties;
@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
InMemoryClientDetailsServiceBuilder build = clients.inMemory();
for (OAuth2ClientsProperties config : oAuth2Properties.getClients()) {
build.withClient(config.getClientId())
.secret(passwordEncoder.encode(config.getClientSecret()))
.accessTokenValiditySeconds(config.getAccessTokenValiditySeconds())
.authorizedGrantTypes("refresh_token", "password", "authorization_code")//OAuth2支持的验证模式
.scopes("user_info")
.autoApprove(true);
}
}
/**
* 密码password模式,需要实现该方法 authenticationManager
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
//扩展token返回结果
if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = new ArrayList();
enhancerList.add(jwtTokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
tokenEnhancerChain.setTokenEnhancers(enhancerList);
//jwt
endpoints.tokenEnhancer(tokenEnhancerChain)
.accessTokenConverter(jwtAccessTokenConverter);
}
endpoints.exceptionTranslator(customWebResponseExceptionTranslator);
}
}
自定义模块
1、自定义登录/注销
1.1、登录页
首先自定义开发一个登录页,并开发接口保证访问url能访问到登录页,例如:/login 。
其次在WebSecurityConfigurerAdapter实现类的configure(HttpSecurity http)方法中,指明自定义登录页页路径 .formLogin().loginPage("/login")
1.2、注销
在登录成功后会生成认证通过的cookie,保证下次跳转到登录页时无需登录就能通过。而注销的操作就是清除该cookie,Spring OAuth2默认的注销地址是:/logout,并且注销成功后会自动重定向到登录页。
修改方式同样也是在WebSecurityConfigurerAdapter实现类的configure(HttpSecurity http)方法中,.logout().addLogoutHandler(new MyLogoutHandler())方法可以自定义注销的实现逻辑,例如MyLogoutHandler()就是我自己实现的处理逻辑,注销成功后会跳转到上一页。
MyLogoutHandler.java
@Component
public class MyLogoutHandler implements LogoutHandler {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
try {
final String refererUrl = request.getHeader("Referer");
response.sendRedirect(refererUrl);//实现自定义重定向
} catch (IOException e) {
e.printStackTrace();
}
}
}
2、自定义token
Spring OAuth2 在登录成功后会生成access_token和refresh_token,但这些token默认是类似于uuid的字符串,我们怎么把他们换成 jwt的token呢?
在之前AuthorizationServerCA.java 类中我们能看到使用jwt方式发放token的配置,包括其中有用到自定义的JwtTokenEnhancer类,可以通过.setAdditionalInformation拓展更多的自定义参数。
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> info = new HashMap<>();
info.put("name","吴晨瑞");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
3、自定义用户验证
在用户输入用户名和密码后,校验是否正常的程序在哪里定义呢?我们一般要自定义类来实现UserDetailsService接口。这个接口里面只有一个方法 loadUserByUsername(String username),传入参数是 用户名,你可以自定义方法获取数据库中该用户名对应的密码,然后Spring Auth2服务会将你数据库中获取的密码和页面上输入的密码比对,判断你是否登录成功。
MyUserDetailsService.java
@Component
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UserFeign userFeign;
@Override
public UserDetails loadUserByUsername(String username) throws BadCredentialsException {
//enable :用户已失效
//accountNonExpired:用户帐号已过期
//credentialsNonExpired:坏的凭证
//accountNonLocked:用户账号已锁定
// return new User("dd", "1", true, true, false, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
String password= userFeign.loadUserByUsername(username);
return new User(username, passwordEncoder().encode(password), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
4、其他
还有一些自定义的登录异常处理、权限异常处理,这里就不一一附上了,可以在github上参考相关代码。Spring OAuth2 有自己一套非常完整的体系,各个接口都可以自定义实现,就像文章开头我附上的那张图一样。如果各位看客感兴趣并且有时间,可以一一实习这些接口,打造一个自己OAuth2单点登录系统。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。