javascript
SpringBoot实现OAuth2认证服务器
一、最簡單認(rèn)證服務(wù)器
1. pom依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId><version>2.1.0.RELEASE</version> </dependency>2. 配置application.yml
security:oauth2:client:client-id: clientIdclient-secret: clientSecretscope: scope1, scope2, scope3, scope4registered-redirect-uri: http://www.baidu.comspring:security:user:name: adminpassword: admin
3. 開啟@EnableAuthorizationServer,同時開啟SpringSecurity用戶登錄認(rèn)證
@SpringBootApplication @EnableAuthorizationServer public class SpringBootTestApplication {public static void main(String[] args) {SpringApplication.run(SpringBootTestApplication.class, args);}@Beanpublic WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {return new WebSecurityConfigurerAdapter() {@Overridepublic void configure(HttpSecurity httpSecurity) throws Exception {httpSecurity.formLogin().and().csrf().disable();}
};
}
}
4. 測試
(1)密碼模式和客戶端模式直接通過單元測試就可以完成
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class SpringBootTestApplicationTest {@Autowiredprivate TestRestTemplate restTemplate;@Testpublic void token_password() {MultiValueMap<String, String> params = new LinkedMultiValueMap<>();params.add("grant_type", "password");params.add("username", "admin");params.add("password", "admin");params.add("scope", "scope1 scope2");String response = restTemplate.withBasicAuth("clientId", "clientSecret").postForObject("/oauth/token", params, String.class);System.out.println(response);}@Testpublic void token_client() {MultiValueMap<String, String> params = new LinkedMultiValueMap<>();params.add("grant_type", "client_credentials");String response = restTemplate.withBasicAuth("clientId", "clientSecret").postForObject("/oauth/token", params, String.class);System.out.println(response);}}(2)授權(quán)碼驗證模式
- 訪問?http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code,跳轉(zhuǎn)到SpringSecurity默認(rèn)的登錄頁面:
-
輸入用戶名/密碼:admin/admin,點擊登錄后跳轉(zhuǎn)到確認(rèn)授權(quán)頁面:
?
-
至少選中一個,然后點擊Authorize按鈕,跳轉(zhuǎn)到 https://www.baidu.com/?code=tg0GDq,這樣我們就拿到了授權(quán)碼。
-
通過授權(quán)碼申請token:?
@Test public void token_code() {MultiValueMap<String, String> params = new LinkedMultiValueMap<>();params.add("grant_type", "authorization_code");params.add("code", "tg0GDq");String response = restTemplate.withBasicAuth("clientId", "clientSecret").postForObject("/oauth/token", params, String.class);System.out.println(response); }
(3)刷新token
@Testpublic void token_refresh() {MultiValueMap<String, String> params = new LinkedMultiValueMap<>();params.add("grant_type", "refresh_token");params.add("refresh_token", "fb00358a-44e2-4679-9129-1b96f52d8d5d");String response = restTemplate.withBasicAuth("clientId", "clientSecret").postForObject("/oauth/token", params, String.class);System.out.println(response);}刷新token功能報錯,// todo 2018-11-08 此處留坑
二、比較復(fù)雜的認(rèn)證服務(wù)器
上面我們搭建的認(rèn)證服務(wù)器存在以下弊端:
針對以上問題,我們要做的就是
接下來我們一步一步實現(xiàn):
1. 創(chuàng)建測試用表及數(shù)據(jù)
drop table if exists test.oauth2_client; create table test.oauth2_client (id int auto_increment primary key,clientId varchar(50),clientSecret varchar(50),redirectUrl varchar(2000),grantType varchar(100),scope varchar(100) );insert into test.oauth2_client(clientId, clientSecret, redirectUrl, grantType, scope) values ('clientId','clientSecret','http://www.baidu.com,http://www.csdn.net', 'authorization_code,client_credentials,password,implicit', 'scope1,scope2');drop table if exists test.oauth2_user; create table test.oauth2_user (id int auto_increment primary key,username varchar(50),password varchar(50) );insert into test.oauth2_user (username, password) values ('admin','admin');insert into test.oauth2_user (username, password) values ('guest','guest'); 創(chuàng)建測試用表及數(shù)據(jù)- 表oauth2_client:存儲clientId、clientSecret及其他信息。本例只創(chuàng)建了一個client。
- 表oauth2_user:用戶信息。本例創(chuàng)建了兩個用戶:admin/admin、guest/guest。
2. Dao和Service
Dao和Service就不用廢話了,肯定要有的
public class Oauth2Client {private int id;private String clientId;private String clientSecret;private String redirectUrl;private String grantType;private String scope;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getClientId() {return clientId;}public void setClientId(String clientId) {this.clientId = clientId;}public String getClientSecret() {return clientSecret;}public void setClientSecret(String clientSecret) {this.clientSecret = clientSecret;}public String getRedirectUrl() {return redirectUrl;}public void setRedirectUrl(String redirectUrl) {this.redirectUrl = redirectUrl;}public String getGrantType() {return grantType;}public void setGrantType(String grantType) {this.grantType = grantType;}public String getScope() {return scope;}public void setScope(String scope) {this.scope = scope;} } Oauth2Client public class Oauth2User {private int id;private String username;private String password;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;} } Oauth2User @Repository public class Oauth2Dao {private final JdbcTemplate jdbcTemplate;@Autowiredpublic Oauth2Dao(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}public List<Oauth2Client> getOauth2ClientByClientId(String clientId) {String sql = "select * from oauth2_client where clientId = ?";return jdbcTemplate.query(sql, new String[]{clientId}, new BeanPropertyRowMapper<>(Oauth2Client.class));}public List<Oauth2User> getOauth2UserByUsername(String username) {String sql = "select * from oauth2_user where username = ?";return jdbcTemplate.query(sql, new String[]{username}, new BeanPropertyRowMapper<>(Oauth2User.class));}} Oauth2Dao @Service public class Oauth2Service {private final Oauth2Dao oauth2Dao;@Autowiredpublic Oauth2Service(Oauth2Dao oauth2Dao) {this.oauth2Dao = oauth2Dao;}public List<Oauth2Client> getOauth2ClientByClientId(String clientId) {return oauth2Dao.getOauth2ClientByClientId(clientId);}public List<Oauth2User> getOauth2UserByUsername(String username) {return oauth2Dao.getOauth2UserByUsername(username);} } Oauth2Service3. 增加pom依賴
因為要使用到數(shù)據(jù)庫以及redis,所以我們需要增加如下依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId> </dependency>4. 修改啟動主類,增加bean注冊
(1)注冊一個PasswordEncoder用于密碼加密:
這樣做的目的是:在我們的應(yīng)用中,可能都多個地方需要我們對用戶的明文密碼進(jìn)行加密。在這里我們統(tǒng)一注冊一個PasswordEncoder,以保證加密算法的一致性。
@Bean public PasswordEncoder passwordEncoder() {return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }(2)注冊一個UserDetailsService用于用戶身份認(rèn)證
@Bean public UserDetailsService userDetailsService(Oauth2Service oauth2Service, PasswordEncoder passwordEncoder) {return username -> {List<Oauth2User> users = oauth2Service.getOauth2UserByUsername(username);if (users == null || users.size() == 0) {throw new UsernameNotFoundException("username無效");}Oauth2User user = users.get(0);String passwordAfterEncoder = passwordEncoder.encode(user.getPassword());return User.withUsername(username).password(passwordAfterEncoder).roles("").build();}; }標(biāo)紅這句代碼大家忽略吧,常理來講數(shù)據(jù)庫中存儲的密碼應(yīng)該就是密文所以這句代碼是不需要的,我比較懶數(shù)據(jù)庫直接存儲明文密碼所以這里需要加密一下。
(3)注冊一個ClientDetailsService用戶clientId和clientSecret驗證
@Bean public ClientDetailsService clientDetailsService(Oauth2Service oauth2Service, PasswordEncoder passwordEncoder) {return clientId -> {List<Oauth2Client> clients1 = oauth2Service.getOauth2ClientByClientId(clientId);if (clients1 == null || clients1.size() == 0) {throw new ClientRegistrationException("clientId無效");}Oauth2Client client = clients1.get(0);String clientSecretAfterEncoder = passwordEncoder.encode(client.getClientSecret());BaseClientDetails clientDetails = new BaseClientDetails();clientDetails.setClientId(client.getClientId());clientDetails.setClientSecret(clientSecretAfterEncoder);clientDetails.setRegisteredRedirectUri(new HashSet<>(Arrays.asList(client.getRedirectUrl().split(","))));clientDetails.setAuthorizedGrantTypes(Arrays.asList(client.getGrantType().split(",")));clientDetails.setScope(Arrays.asList(client.getScope().split(",")));return clientDetails;}; }標(biāo)紅代碼忽略,理由同上。
關(guān)于BaseClientDetails的屬性,這里要啰嗦幾句:它繼承于接口ClientDetails,該接口包含如下屬性:
- getClientId:clientId,唯一標(biāo)識,不能為空
- getClientSecret:clientSecret,密碼
- isSecretRequired:是否需要驗證密碼
- getScope:可申請的授權(quán)范圍
- isScoped:是否需要驗證授權(quán)范圍
- getResourceIds:允許訪問的資源id,這個涉及到資源服務(wù)器
- getAuthorizedGrantTypes:可使用的Oauth2授權(quán)模式,不能為空
- getRegisteredRedirectUri:回調(diào)地址,用戶在authorization_code模式下接收授權(quán)碼code
- getAuthorities:授權(quán),這個完全等同于SpringSecurity本身的授權(quán)
- getAccessTokenValiditySeconds:access_token過期時間,單位秒。null等同于不過期
- getRefreshTokenValiditySeconds:refresh_token過期時間,單位秒。null等同于getAccessTokenValiditySeconds,0或者無效數(shù)字等同于不過期
- isAutoApprove:判斷是否獲得用戶授權(quán)scope
?(4)注冊一個TokenStore以保存token信息
@Bean public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {return new RedisTokenStore(redisConnectionFactory); }(5)注冊一個AuthorizationCodeServices以保存authorization_code的授權(quán)碼code
生成一個RandomValueAuthorizationCodeServices的bean,而不是直接生成AuthorizationCodeServices的bean。RandomValueAuthorizationCodeServices可以幫我們完成code的生成過程。如果你想按照自己的規(guī)則生成授權(quán)碼code請直接生成AuthorizationCodeServices的bean。
@Bean public AuthorizationCodeServices authorizationCodeServices(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, OAuth2Authentication> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);redisTemplate.afterPropertiesSet();return new RandomValueAuthorizationCodeServices() {@Overrideprotected void store(String code, OAuth2Authentication authentication) {redisTemplate.boundValueOps(code).set(authentication, 10, TimeUnit.MINUTES);}@Overrideprotected OAuth2Authentication remove(String code) {OAuth2Authentication authentication = redisTemplate.boundValueOps(code).get();redisTemplate.delete(code);return authentication;}}; }(6)注冊一個AuthenticationManager用來password模式下用戶身份認(rèn)證
直接使用上面注冊的UserDetailsService來完成用戶身份認(rèn)證。
@Bean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();provider.setUserDetailsService(userDetailsService);provider.setPasswordEncoder(passwordEncoder);return new ProviderManager(Collections.singletonList(provider)); }(7)配置認(rèn)證服務(wù)器
上面注冊了這么多bean,到了他們發(fā)揮作用的時候了
@Bean public AuthorizationServerConfigurer authorizationServerConfigurer(UserDetailsService userDetailsService, ClientDetailsService clientDetailsService,TokenStore tokenStore, AuthorizationCodeServices authorizationCodeServices, AuthenticationManager authenticationManager) {return new AuthorizationServerConfigurer() {@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.withClientDetails(clientDetailsService);}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.userDetailsService(userDetailsService);endpoints.tokenStore(tokenStore);endpoints.authorizationCodeServices(authorizationCodeServices);endpoints.authenticationManager(authenticationManager);}}; }
5. 修改配置文件,配置數(shù)據(jù)庫及redis連接
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://192.168.2.12:3306/test?characterEncoding=utf8username: rootpassword: onceasredis:host: 192.168.2.12port: 6379password: 1234566.測試
(1)密碼模式和客戶端模式同上
(2)授權(quán)碼驗證模式
- 訪問 http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code&scope=scope1 scope2&redirect_uri=http://www.baidu.com,跳轉(zhuǎn)到SpringSecurity默認(rèn)的登錄頁面:
-
輸入用戶名/密碼:admin/admin,點擊登錄后跳轉(zhuǎn)到確認(rèn)授權(quán)頁面:
?
-
至少選中一個,然后點擊Authorize按鈕,跳轉(zhuǎn)到 https://www.baidu.com/?code=tg0GDq,這樣我們就拿到了授權(quán)碼。
-
通過授權(quán)碼申請token:?
@Test public void token_code() {MultiValueMap<String, String> params = new LinkedMultiValueMap<>();params.add("grant_type", "authorization_code");params.add("code", "tg0GDq");String response = restTemplate.withBasicAuth("clientId", "clientSecret").postForObject("/oauth/token", params, String.class); System.out.println(response); }
(3)刷新token
申請的所有token中都沒有返回refresh_token,// todo 2018-11-08 此處留坑
三、自定義頁面
?
1. 自定義用戶登錄頁面
用戶登錄頁面就是SpringSecurity的默認(rèn)登錄頁面,所以按照SpringSecurity的規(guī)則更改即可,可參照https://www.cnblogs.com/LOVE0612/p/9897647.html里面的相關(guān)內(nèi)容
2. 自定義用戶授權(quán)頁面
用戶授權(quán)頁面是/oauth/authorize轉(zhuǎn)發(fā)給/oauth/confirm_access然后才呈現(xiàn)最終頁面給用戶的。所以想要自定義用戶授權(quán)頁面,用戶點擊Authorize按鈕時會通過form表單發(fā)送請求:
Request URL: http://127.0.0.1:8080/oauth/authorize Request Method: POSTFormData user_oauth_approval: true scope.scope1: true scope.scope2: true所以我們要自定義用戶授權(quán)頁面,我們只要重新定義一個mapping即可并按照上述要求完成post請求即可。
(1)增加pom依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>(2)Controller
@Controller public class Oauth2Controller {@GetMapping("oauth/confirm_access")public String authorizeGet() {return "oauth/confirm_access";} }(3)創(chuàng)建/resources/templates/oauth/confirm_access.html
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>my authorize page</title> </head> <body> <form action="/oauth/authorize" method="post"><input type="hidden" name="user_oauth_approval" value="true"><div id="scope"></div><input type="submit" value="授權(quán)"> </form> <script>function getQueryString(name) {var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");var r = window.location.search.substr(1).match(reg);if (r != null) return unescape(r[2]);return null;} </script> <script>var scope = getQueryString("scope");var scopeList = scope.split(" ");var html = "";for (var i = 0; i < scopeList.length; i++) {html += scopeList[i] + ":<input type='checkbox' name='scope." + scopeList[i] + "' value='true'/><br />";}document.getElementById("scope").innerHTML = html; </script> </body> </html>3. 自定義錯誤頁面
與上面同理,重新定義一個mapping對應(yīng)uri:/oauth/error,可通過?Object error = request.getAttribute("error"); 獲取錯誤信息,具體html頁面內(nèi)容就不再贅述了。
四、支持Restfull風(fēng)格
如果考慮前后分離呢?那么流程應(yīng)該是:
轉(zhuǎn)載于:https://www.cnblogs.com/LOVE0612/p/9913336.html
總結(jié)
以上是生活随笔為你收集整理的SpringBoot实现OAuth2认证服务器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mybatis {arg0} 与 {0}
- 下一篇: 关于 JS 模块化的最佳实践总结