共计 7519 个字符,预计需要花费 19 分钟才能阅读完成。
概述
在第一篇中,我们已经讲过了 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 单点登录系统。