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