今日干货
下文有任何看不懂的地方,建议一定先看下相关系列:
「Spring Security 系列:」
「OAuth2 系列:」
好了,开始今天的正文。
单点登录是我们在分布式系统中很常见的一个需求。
分布式系统由多个不同的子系统组成,而我们在使用系统的时候,只需要登录一次即可,这样其他系统都认为用户已经登录了,不用再去登录。前面和小伙伴们分享了 OAuth2+JWT 的登录方式,这种无状态登录实际上天然的满足单点登录的需求,可以参考:想让 OAuth2 和 JWT 在一起愉快玩耍?请看松哥的表演。
当然大家也都知道,无状态登录也是有弊端的。
所以今天松哥想和大家说一说 Spring Boot+OAuth2 做单点登录,利用 @EnableOAuth2Sso 注解快速实现单点登录功能。
松哥依然建议大家在阅读本文时,先看看本系列前面的文章,这有助于更好的理解本文。
前面的案例中,松哥一直都把授权服务器和资源服务器分开创建,今天这个案例,为了省事,我就把授权服务器和资源服务器搭建在一起(不过相信大家看了前面的文章,应该也能自己把这两个服务器拆分开)。
所以,今天我们一共需要三个服务:
项目 | 端口 | 描述 |
---|---|---|
auth-server | 1111 | 授权服务器+资源服务器 |
client1 | 1112 | 子系统1 |
client2 | 1113 | 子系统2 |
auth-server 用来扮演授权服务器+资源服务器的角色,client1 和 client2 则分别扮演子系统的角色,将来等 client1 登录成功之后,我们也就能访问 client2 了,这样就能看出来单点登录的效果。
我们创建一个名为 oauth2-sso 的 Maven 项目作为父工程即可。
接下来我们来搭建统一认证中心。
首先我们创建一个名为 auth-server 的 module,创建时添加如下依赖:
项目创建成功之后,这个模块由于要扮演授权服务器+资源服务器的角色,所以我们先在这个项目的启动类上添加 @EnableResourceServer 注解,表示这是一个资源服务器:
@SpringBootApplication
@EnableResourceServer
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
接下来我们进行授权服务器的配置,由于资源服务器和授权服务器合并在一起,因此授权服务器的配置要省事很多:
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("javaboy")
.secret(passwordEncoder.encode("123"))
.autoApprove(true)
.redirectUris("http://localhost:1112/login", "http://localhost:1113/login")
.scopes("user")
.accessTokenValiditySeconds(7200)
.authorizedGrantTypes("authorization_code");
}
}
这里我们只需要简单配置一下客户端的信息即可,这里的配置很简单,前面的文章也讲过了,大家要是不懂,可以参考本系列前面的文章:这个案例写出来,还怕跟面试官扯不明白 OAuth2 登录流程?。
当然这里为了简便,客户端的信息配置是基于内存的,如果大家想将客户端信息存入数据库中,也是可以的,参考:OAuth2 令牌还能存入 Redis ?越玩越溜!
接下来我们再来配置 Spring Security:
@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login.html", "/css/**", "/js/**", "/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login")
.antMatchers("/oauth/authorize")
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("sang")
.password(passwordEncoder().encode("123"))
.roles("admin");
}
}
关于 Spring Security 的配置,如果小伙伴们不懂,可以看看松哥最近正在连载的 Spring Security 系列。
我这里来大致捋一下:
SecurityConfig 和 AuthServerConfig 都是授权服务器需要提供的东西(如果小伙伴们想将授权服务器和资源服务器拆分,请留意这句话),接下来,我们还需要提供一个暴露用户信息的接口(如果将授权服务器和资源服务器分开,这个接口将由资源服务器提供):
@RestController
public class UserController {
@GetMapping("/user")
public Principal getCurrentUser(Principal principal) {
return principal;
}
}
最后,我们在 application.properties 中配置一下项目端口:
server.port=1111
另外,松哥自己提前准备了一个登录页面,如下:
将登录页面相关的 html、css、js 等拷贝到 resources/static 目录下:
这个页面很简单,就是一个登录表单而已,我把核心部分列出来:
<form action="/login" method="post">
<div class="input">
<label for="name">用户名</label>
<input type="text" name="username" id="name">
<span class="spin"></span>
</div>
<div class="input">
<label for="pass">密码</label>
<input type="password" name="password" id="pass">
<span class="spin"></span>
</div>
<div class="button login">
<button type="submit">
<span>登录</span>
<i class="fa fa-check"></i>
</button>
</div>
</form>
注意一下 action 提交地址不要写错即可。
「文末可以下载源码。」
如此之后,我们的统一认证登录平台就算是 OK 了。
接下来我们来创建一个客户端项目,创建一个名为 client1 的 Spring Boot 项目,添加如下依赖:
项目创建成功之后,我们来配置一下 Spring Security:
@Configuration
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().csrf().disable();
}
}
这段配置很简单,就是说我们 client1 中所有的接口都需要认证之后才能访问,另外添加一个 @EnableOAuth2Sso 注解来开启单点登录功能。
接下来我们在 client1 中再来提供一个测试接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.getName() + Arrays.toString(authentication.getAuthorities().toArray());
}
}
这个测试接口返回当前登录用户的姓名和角色信息。
接下来我们需要在 client1 的 application.properties 中配置 oauth2 的相关信息:
security.oauth2.client.client-secret=123
security.oauth2.client.client-id=javaboy
security.oauth2.client.user-authorization-uri=http://localhost:1111/oauth/authorize
security.oauth2.client.access-token-uri=http://localhost:1111/oauth/token
security.oauth2.resource.user-info-uri=http://localhost:1111/user
server.port=1112
server.servlet.session.cookie.name=s1
这里的配置也比较熟悉,我们来看一下:
如此之后,我们的 client1 就算是配置完成了。
按照相同的方式,我们再来配置一个 client2,client2 和 client1 一模一样,就是 cookie 的名字不同(随意取,不相同即可)。
接下来,我们分别启动 auth-server、client1 和 client2,首先我们尝试去方式 client1 中的 hello 接口,这个时候会自动跳转到统一认证中心:
然后输入用户名密码进行登录。
登录成功之后,会自动跳转回 client1 的 hello 接口,如下:
此时我们再去访问 client2 ,发现也不用登录了,直接就可以访问:
OK,如此之后,我们的单点登录就成功了。
最后,我再来和小伙伴们把上面代码的一个执行流程捋一捋:
OK,本文和小伙伴们聊了一些 SpringBoot +OAuth2 单点登录的问题,完整案例下载地址:https://github.com/lenve/oauth2-samples