SpringBoot2 配置 Oauth2
前言
自己用Springboot2 配置 Oauth2 的时候,参照了网上的很多例子,不过大部分例子都特别繁琐,而且有些地方写的不是很详细,坑太多,非常不适合第一次配置 Oauth2 的。所以我准备写一个最精简的配置 Oauth2 的文章。
Oauth2 的简介这里就不写了,不懂的可以直接百度。本文章注重于Oauth2的基本配置,不注重原理。
依赖
<!-- SpringSecurity -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 由于一些注解和API从spring security5.0中移除,所以需要导入下面的依赖包 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
这里持久层框架用的 JPA ,这个不影响,不喜欢的可以自己换。
数据源的配置
server.port=80
spring.datasource.url=jdbc:mysql://192.168.1.250:3306/spring_oauth2?useSSL=false&serverTimezone=Hongkong
spring.datasource.username: root
spring.datasource.password: 123456
spring.datasource.driver-class-name: com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.main.allow-bean-definition-overriding=true
配置 WebSecurity
@Configuration //配置类注解
@EnableWebSecurity //开启WebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启方法级的注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//引入UserServiceDetail
@Autowired
UserServiceDetail userServiceDetail;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 允许所有人访问 '/oauth' 以下的目录
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated();
http.csrf().disable() //关闭 csrf
.and()
.authorizeRequests()
.antMatchers("/**").authenticated() //其他目录需要认证
.and()
.httpBasic(); //开启基本http验证
}
// 把 PasswordEncoder 放到 Spring 容器中
// Springboot2 貌似必须把这个配置到 Spring 容器中,不然会报错
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
}
//把 AuthenticationManager 配置到 Spring 容器中,配置Oauth2 的时候会用到
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
实体类
User 类
@Entity
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false,unique = true)
private String username;
@Column
private String password;
@ManyToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
@JoinTable(name="user_role",joinColumns = @JoinColumn(name = "user_id",referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name="role_id",referencedColumnName = "id")
)
private List<Role> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
}
角色类
@Entity
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Override
public String getAuthority() {
return name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
}
UserDao
public interface UserDao extends JpaRepository<User,Long> {
User findByUsername(String username);
}
UserServiceDetail
- UserServiceDetail 需要实现org.springframework.security.core.userdetails.UserDetailsService 包下的 UserDetailsService 接口。
- 重写 loadUserByUsername 方法。该方法应该返回用户的基本信息,包括权限权限信息,即 UserDetails 对象。
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails userDetails = userDao.findByUsername(username);
return userDetails;
}
}
配置 Oauth2
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
//引入 SpringSecurity 中配置的 AuthenticationManager
@Autowired
private AuthenticationManager authenticationManager;
//引入 UserServiceDetail 服务
@Autowired
private UserServiceDetail userServiceDetail;
//配置客户端信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//这里直接把配置信息保存在内存中
clients.inMemory()
.withClient("service-hi")
//这里必须使用加密
.secret(new BCryptPasswordEncoder().encode("123456"))
//配置 GrantTypes
//支持 刷新token
// 使用密码模式
.authorizedGrantTypes("client_credentials","refresh_token","password")
//这个随便配了一个,暂时没用到
.scopes("server");
}
//配置 Token 的节点 和 Token 服务
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.authenticationManager(authenticationManager)
.userDetailsService(userServiceDetail)
.accessTokenConverter(jwtTokenEnhancer());
}
// 配置 Token 节点的安全策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
//使用 jwt
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtTokenEnhancer());
}
// 配置 jwt 生成 策略
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123456"); //密钥
return converter;
}
}
如果 token 使用 内存形式的话,更简单
直接 把上面的 tokenStore方法改为
@Bean
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
并删除 jwtTokenEnhancer 方法。
最终测试
测试这里,我要写的详细一些,之前踩过很多坑。
启动项目以后,在日志中可以看到下面三个地址
[Ant [pattern=’/oauth/token’],
Ant [pattern=’/oauth/token_key’],
Ant [pattern=’/oauth/check_token’]]
这里分别说一下,并进行测试。
- /oauth/token: 这个是获取 以及 刷新 token 的地址
- /oauth/token_key: 这个我自己测试了一下,应该是返回 客户端密码以及加密方式(可能不对,欢迎大佬指正)
- /oauth/check_token:这个是验证 token 是否正确的地址,如果正确则返回用户基本信息
获取token
请求方式
-
请求地址:localhost/oauth/token
-
请求方法: POST
-
请求参数:
1. grant_type,我们使用的是密码模式,所以这里设置成 password
2. username, 用户名
3. password,密码 -
请求头:
请求头中,需要传入一个Authorization 参数。
它的值为 Oauth2 配置中的客户端的用户名与密码以 '用户名:密码’的形式组合,然后再进行 Base64 编码得到的。
本例中 客户端的用户名为 server-hi,密码为 123456,将它们组合为 server-hi:123456,进行 Base64 编码,得到 “c2VydmljZS1oaToxMjM0NTY=”。
访问的时候请求头中的参数名为 ‘Authorization’ ,参数值为 ‘Basic c2VydmljZS1oaToxMjM0NTY=’,注意参数值中 Basic 与 Base64编码后的字符串中间有空格
请求测试
使用postman ,按上面的方式进行测试。
在请求头中传入 Authorization 参数
返回的结果
验证token
请求方式
- 请求地址: localhost/oauth/check_token
- 请求方法:GET
- 请求参数:
token,传入之前获取的token - 请求头中也需要添加上面的验证(参照获取token)
测试
传入 token 参数
请求头中传入 Authorization 参数
返回的结果
访问受保护的资源
请求方式
有两种方式
-
在请求参数中传入 access_token 参数。值为 之前获取到的token
-
在请求头中传入 Authorization参数。值为 bearer + 空格 + 之前获取的token
其中的bearer ,是获取token 时,返回的 token_type 的值。
请求地址
有两种方式访问被保护的资源
1.传入 access_token 参数
本例测试 返回 该当前用户的用户名.
- 在 请求头中传入 Authorization
返回结果与第一种一样
刷新 token
请求方式
-
请求地址:localhost/oauth/token
-
请求方法: POST
-
请求参数:
1. grant_type, 值为 refresh_token
2. refresh_token, 值为 获取token 时,返回的refresh_token的值 -
请求头:请求头中也需要添加上面的验证(参照获取token)
测试
分别传入 grant_type ,以及 refresh_token
在请求头中传入 Authorization
总结
其实 SpringBoot 配置 Oauth2 不是特别难,之前出了问题,我一直以为是我自己配错了,最后才发现请求的方式不对,所以这里在写测试的时候,写得比较详细。