https://projects.spring.io/spring-security-oauth/docs/oauth2.html
https://github.com/royclarkson/spring-rest-service-oauth
https://github.com/spring-projects/spring-security-oauth/blob/master/tests/annotation/jdbc/src/main/resources/schema.sql
https://github.com/spring-projects/spring-boot/issues/8478
适应场景
为Spring Restful接口添加OAuth2 token机制,适用手机APP的后台,以及前后端分离下的后台接口。
添加依赖
创建Spring Boot项目(1.5.9.RELEASE),POM中添加:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
token可以保存在内存、数据库、Redis中,此处我们选择保存到MySql数据库,DDL如下:
CREATE TABLE `oauth_access_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(255) NOT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(255) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(255) NOT NULL,
`token` blob,
`authentication` blob,
PRIMARY KEY (`token_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
client也可以放到数据库中,此处我们将client放在内存中,如果需要保存到数据库,DDL可以参考这里
配置
- OAuth2相关配置
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
/**
* OAuth2相关配置:使用默认的DefaultTokenServices,token保存到数据库,以及token超时时间设置
*/
@Configuration
public class OAuth2ServerConfiguration {
private static final String RESOURCE_ID = "RS_DEMO";
private static final String CLIENT_ID = "APP";
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("dataSource")
private DataSource dataSource;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
@Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
// @formatter:off
endpoints
.tokenStore(tokenStore())
.authenticationManager(this.authenticationManager)
.userDetailsService(userDetailsService);
// @formatter:on
}
/**
* 1. client信息放在内存中,ID和Secret设置为固定值
* 2. 设置access_token和refresh_token的超时时间
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// @formatter:off
clients
.inMemory()
.withClient(CLIENT_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("read", "write", "trust")
.resourceIds(RESOURCE_ID)
.secret("secret")
.accessTokenValiditySeconds(60*60*12)
.refreshTokenValiditySeconds(60*60*24*30);
// @formatter:on
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore());
return tokenServices;
}
}
}
- WEB Security配置,此处配置所有
/auth/*
的URL需要access_token,/anon/*
的URL不需要token也能访问。如果需要跨域,Chrome浏览器会先发送OPTIONS方法来检查接口是否支持跨域访问,所以这里也添加了对OPTIONS请求不验证TOKEN。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 权限相关配置:注册密码算法,设置UserDetailsService
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/auth/*").authenticated();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 允许OPTIONS方法访问,不做auth验证,因为的跨域开发是,Chrome浏览器会先发送OPTIONS再真正执行GET POST
// 如果正式发布时在同一个域下,请去掉`and().ignoring().antMatchers(HttpMethod.OPTIONS)`
web.ignoring().antMatchers("/anon/**").and().ignoring().antMatchers(HttpMethod.OPTIONS);
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
- UserSerice,需要实现
org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@Service("userDetailsService")
public class UserService implements UserDetailsService{
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectByIfkCode(username);
if(user == null){
throw new UsernameNotFoundException("用户不存在!");
}
return new CustomUser(user);
}
}
实现Spring的UserDetails
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
* UserDetailsService返回当前对象,该对象包含User实体,User必须实现Serializable
*/
public class CustomUser extends User implements UserDetails {
private User user = null;
public CustomUser(User user) {
super(user);
this.user = user;
}
public User getUser(){
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 此处可以设置用户的具体权限,这里我们返回空集合
return Collections.emptySet();
}
@Override
public String getUsername() {
return super.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
User实体
public class User implements Serializable{
private static final long serialVersionUID = -7810168718373868640L;
public User() {}
public User(User user) {
super();
this.userId = user.getUserId();
this.userName = user.getUserName();
this.mobile = user.getMobile();
this.password = user.getPassword();
this.email = user.getEmail();
}
private Long userId;
private String password;
private String userName;
private String mobile;
private String email;
// .... set get...
}
Controller
通过@AuthenticationPrincipal
获取用户信息,如果无法获取用户信息,在application.properties中添加:
security.oauth2.resource.filter-order=3
@Controller
public class StudentController {
@RequestMapping(value = "/anon/student2", method = RequestMethod.GET)
@ResponseBody
public Student student2(Long orgId) {
Student stu = new Student();
stu.setOrgId(orgId);
return stu;
}
@RequestMapping("/auth/student3")
@ResponseBody
public Map<String, Object> student3(@AuthenticationPrincipal User user) {
Map map = new HashMap();
map.put("s1", "s1");
map.put("s2", "s2");
if (user != null) {
map.put("s3", user.getPassword());
}
return map;
}
}
访问接口
http://localhost:8088/oauth/token?username=T0000053&password=111111&grant_type=password&scope=read%20write%20trust
获取access_token和refresh_token,Headers中的Authorization是clientId:secret做Base64的结果
刷新token
http://localhost:8088/oauth/token?grant_type=refresh_token&scope=read%20write%20trust&refresh_token=df31b988-ccb3-42ae-a61b-cae5ad44e939
访问受保护接口
Header添加access_token,Authorization:Bearer 69ffcef9-023a-4685-ace9-faf3dc5cef17