内容简介:use JWT OAuth2 and spring-security Create AuthorizationServer
常常要做授權系統都要重頭做或是你需要做 SSO 卻又有安全性考量,來參考看看使用 SpringSecurity 與 JWT 跟 OAuth2 來做個授權系統吧
什麼是 JWT ?
傳統作法登入之後是將登入資訊放在所謂 Server 端的記憶體中也就是所謂的 Session,方便又快速達成管理,但也帶來了 服務是有狀態的缺點
服務有狀態到底是會有什麼問題?
你的 Session 服務一重開就沒了、或是當你有附載平衡的時候你必須處理 Session 同步問題。
上面問題其實都還是可以解的,但為什麼要改變就繼續往下看
既然服務有狀態很難去維護,那把這些 Session 資訊改放在資料庫如何?
IO 會增加、資料庫也會有瓶頸問題。
好啦~~那我放 Redis 總沒問題吧?
這當然也是解法之一,Redis 很快,又可以分散儲存,但為何要浪費頻寬在僅僅只是查詢這串 Token 代表誰? 如果有個可信任的 Token 並可以代表用戶資訊,那不就太完美了
所以 JWT 規格就是因為這樣的需求產生。
JWT 官網
https://jwt.io/OAuth2 又要幹嘛?
OAuth 是一種授權機制
上面我們從 Token 進化成了 JWT,但 JWT 也不是無缺點,舉例來說 JWT 是沒辦法取消授權的,你只能等他自然過期,所以你被迫只能發1小時用的 JWT TOKEN,以免出現明明是停權的用戶但還是可以網站的功能囧況,但哪種人有辦法接受每小時登入呢?
這種問題則是要透過機制面來改善
這邊有很詳細的 Oauth 說明 OAuth 2.0 筆記 (1) 世界觀 但目前我們不會全部都用到 目前只用以下兩個 可以參考看看
首先你必須清楚知道角色
Resource Owner- 可以授權別人去存取 Protected Resource 。如果這個角色是人類的話,則就是指使用者 (end-user)。
Resource Server- 存放 Protected Resource 的伺服器,可以根據 Access Token 來接受 Protected Resource 的請求。
Client- 代表 Resource Owner 去存取 Protected Resource 的應用程式。 “Client” 一詞並不指任何特定的實作方式(可以在 Server 上面跑、在一般電腦上跑、或是在其他的設備)。
Authorization Server- 在認證過 Resource Owner 並且 Resource Owner 許可之後,核發 Access Token 的伺服器。
Implicit Grant Flow
是你常見的像 FB 那樣,當別人的問券或是網站要用的你資料,則會回到 FB 取得授權後才能繼續玩
關於 Implicit Grant Flow 注意幾點
Authorization Server 直接向 Client 核發 Access Token (一步)。
適合非常特定的 Public Clients ,例如跑在 Browser 裡面的應用程式。
Authorization Server 不必(也無法)驗證 Client 的身份。
禁止核發 Refresh Token。
需要 User-Agent Redirection。
有資料外洩風險
Resource Owner Credentials Grant Flow
是比較會偏內部可信任的應用在取得授權,因為會經手用戶的帳號密碼
關於 Resource Owner Credentials Grant Flow 注意幾點
Resource Owner 的帳號密碼直接拿來當做 Grant。
適用於 Resource Owner 高度信賴的 Client (像是 OS 內建的)或是官方應用程式。
其他流程不適用時才能用。
可以核發 Refresh Token。
沒有 User-Agent Redirection。
基於 OAuth2 跟 JWT 各有專門的地方,我們當然可以強強聯手打造更安全的授權方式
這邊使用 Spring Security 來做這套授權系統,那為什麼要用?
Spring 強項就是在不同的套件組合,今天你不想用 MySQL 了,沒問題馬上實作一個 TokenStore 想存 Redis、ES 都很方便
或是公司要用 LDAP 都有對應的套件來省時省力
而且是 Security 嘛 當然安全功能當然齊全
諸如 Configure CSRF ProtectionX-XSS-ProtectionSpring Security的response header 都很齊全
其實不只 Auth 服務才需要,一般專案都可以用它來做一些基本防護,弱掃至少可以少了大半的問題。
嘛~~雖然 Security 套件分太細要學會也很煩,但學會怎麼用總還是比自己做的安全跟省時吧
資料庫表格
這邊使用自訂的用戶資料表
CREATE TABLE `account` ( `accountid` varchar(10) NOT NULL, `username` varchar(30) NOT NULL, `password` varchar(60) NOT NULL, `enabled` bit(1) NOT NULL, `expired` bit(1) NOT NULL, `locked` bit(1) NOT NULL, `credentialsexpired` bit(1) NOT NULL, `createddate` datetime NOT NULL, `createdby` varchar(20) DEFAULT NULL, `lastmodifieddate` datetime NOT NULL, `lastmodifiedby` varchar(20) DEFAULT NULL, PRIMARY KEY (`accountid`), UNIQUE KEY `username` (`username`) ) CREATE TABLE `role` ( `roleid` varchar(10) NOT NULL, `code` varchar(20) NOT NULL, `label` varchar(50) NOT NULL, `createddate` datetime NOT NULL, `createdby` varchar(10) DEFAULT NULL, `lastmodifieddate` datetime NOT NULL, `lastmodifiedby` varchar(10) DEFAULT NULL, PRIMARY KEY (`roleid`), UNIQUE KEY `code` (`code`) ) CREATE TABLE `account_role` ( `serid` varchar(10) NOT NULL, `accountid` varchar(10) NOT NULL, `roleid` varchar(10) NOT NULL, `createddate` datetime NOT NULL, `createdby` varchar(10) DEFAULT NULL, `lastmodifieddate` datetime NOT NULL, `lastmodifiedby` varchar(10) DEFAULT NULL, PRIMARY KEY (`serid`), KEY `account_id` (`accountid`), KEY `role_id` (`roleid`), CONSTRAINT `account_role_ibfk_1` FOREIGN KEY (`accountid`) REFERENCES `account` (`accountid`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `account_role_ibfk_2` FOREIGN KEY (`roleid`) REFERENCES `role` (`roleid`) ON DELETE NO ACTION ON UPDATE NO ACTION ) CREATE TABLE `oauthtoken` ( `serid` varchar(32) NOT NULL, `tokenid` varchar(32) NOT NULL, `refreshid` varchar(32) DEFAULT NULL, `clientid` varchar(50) NOT NULL, `granttype` varchar(50) NOT NULL, `resourceids` text, `scopes` text, `username` varchar(255) DEFAULT NULL, `redirecturi` varchar(255) DEFAULT NULL, `accesstoken` blob NOT NULL, `refreshtoken` blob, `refreshed` bit(1) NOT NULL, `locked` bit(1) NOT NULL, `authentication` blob NOT NULL, `createddate` datetime NOT NULL, `createdby` varchar(255) DEFAULT NULL, `lastmodifieddate` datetime NOT NULL, `lastmodifiedby` varchar(255) DEFAULT NULL, PRIMARY KEY (`serid`), UNIQUE KEY `tokenid` (`tokenid`) ) INSERT INTO `account` VALUES ('sdf34erh', 'papidakos', '$2a$10$5RcLNvOD.r03ZayV53MxVuUQYbJvDtMrRFTTUVL9G2X0S6/ZxEo3S', '', '\0', '\0', '\0', '2017-02-16 10:41:40', null, '2017-02-16 10:41:44', null); INSERT INTO `role` VALUES ('asdasd', 'ROLE_USER', 'User', '2017-02-16 10:42:13', null, '2017-02-16 10:42:18', null); INSERT INTO `account_role` VALUES ('sdcsd', 'sdf34erh', 'asdasd', '2017-02-16 10:42:40', null, '2017-02-16 10:42:43', null);
專案
buildscript { ext { springBootVersion = '1.5.1.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' jar { baseName = 'auth' version = '0.0.1-SNAPSHOT' } sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile('org.springframework.boot:spring-boot-starter-data-jpa') compile('org.springframework.boot:spring-boot-starter-security') compile('org.springframework.boot:spring-boot-starter-web') compile 'org.springframework.security.oauth:spring-security-oauth2:2.0.12.RELEASE' compile 'org.springframework.security:spring-security-jwt:1.0.7.RELEASE' compile 'org.apache.commons:commons-lang3:3.5' compile 'org.modelmapper:modelmapper:0.7.7' compile 'mysql:mysql-connector-java:6.0.5' compile 'io.jsonwebtoken:jjwt:0.7.0' compileOnly('org.projectlombok:lombok') testCompile('org.springframework.boot:spring-boot-starter-test') }
io.jsonwebtoken:jjwt 在 AuthService 中其實不用,是為了 Decode JWT 的 test 加進來
org.apache.commons:commons-lang3 非必要 是用在 自己實作的 TokenStore
org.modelmapper:modelmapper 非必用 用在物件轉換
OAuth 流程
其實 Spring Security 有個預設的流程 org.springframework.security.oauth2.provider.token.DefaultTokenService 可以去看看
但我們不用修改這套流程
實作 TokenStore
Spring Security 預設的 org.springframework.security.oauth2.provider.token.store.JdbcTokenStore 管理方式是 Single sign-on 也就是會踢掉前一次登入的 Token ,但是這並不符合我們要的
當你是登入的時候,會依照上面 DefaultTokenServices 的流程跑這幾個方法
getAccessToken >> storeAccessToken >> storeRefreshToken
當你是 Refresh Token 的時候會依序執行以下方法
readRefreshToken >> readAuthenticationForRefreshToken >> removeAccessTokenUsingRefreshToken >> storeAccessToken
所以我們實作以上幾個動作就可以了
package com.ps.security; import com.ps.model.Oauthtoken; import com.ps.repository.OauthtokenRepository; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenStore; import java.io.*; import java.util.Collection; import java.util.Collections; /** * 新 Token 儲存 * >> getAccessToken >> storeAccessToken >> storeRefreshToken * <p> * 更新 Token * >> readRefreshToken >> readAuthenticationForRefreshToken >> removeAccessTokenUsingRefreshToken >> storeAccessToken * <p> * Created by samchu on 2017/2/15. */ @Slf4j public class CustomTokenStore implements TokenStore { @Autowired private OauthtokenRepository oauthtokenRepository; @Override public OAuth2Authentication readAuthentication(OAuth2AccessToken token) { log.debug(">> CustomTokenStore.readAuthentication token={}", token); return null; } @Override public OAuth2Authentication readAuthentication(String token) { log.debug(">> CustomTokenStore.readAuthentication token={}", token); return null; } /** * 儲存 Token ,Auth 跟 Refresh 都會使用 * * @param token * @param authentication */ @Override public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { log.debug(">> CustomTokenStore.storeAccessToken token={}, authentication={}", token, authentication); Oauthtoken oauthtoken = new Oauthtoken(); String serid = RandomStringUtils.randomAlphanumeric(32); oauthtoken.setSerid(serid); oauthtoken.setTokenid(DigestUtils.md5Hex(token.getValue())); oauthtoken.setRefreshid(token.getRefreshToken() == null ? null : DigestUtils.md5Hex(token.getRefreshToken().getValue())); oauthtoken.setClientid(authentication.getOAuth2Request().getClientId()); oauthtoken.setGranttype(authentication.getOAuth2Request().getGrantType()); oauthtoken.setResourceids(authentication.getOAuth2Request().getResourceIds().toString()); oauthtoken.setScopes(authentication.getOAuth2Request().getScope().toString()); oauthtoken.setUsername(authentication.isClientOnly() ? null : authentication.getName()); oauthtoken.setRedirecturi(authentication.getOAuth2Request().getRedirectUri()); oauthtoken.setAccesstoken(token.getValue()); oauthtoken.setRefreshtoken(token.getRefreshToken() == null ? null : token.getRefreshToken().getValue()); oauthtoken.setRefreshed(Boolean.FALSE); oauthtoken.setLocked(Boolean.FALSE); try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(authentication); oos.flush(); oauthtoken.setAuthentication(baos.toByteArray()); } catch (IOException e) { log.error("OAuth2Authentication serialization error", e); } oauthtokenRepository.save(oauthtoken); log.debug("<< CustomTokenStore.storeAccessToken"); } @Override public OAuth2AccessToken readAccessToken(String tokenValue) { log.debug(">> CustomTokenStore.readAccessToken tokenValue={}", tokenValue); return null; } @Override public void removeAccessToken(OAuth2AccessToken token) { log.debug(">> CustomTokenStore.removeAccessToken token={}", token); } @Override public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) { log.debug(">> CustomTokenStore.storeRefreshToken refreshToken={}, authentication={}", refreshToken, authentication); } /** * 讀取 RefreshToken 這段只有在 Refresh 的時候被呼叫到,並且做限制 RefreshToken 使用過後就不能在取得新的 Token * * @param tokenValue * @return */ @Override public OAuth2RefreshToken readRefreshToken(String tokenValue) { log.debug(">> CustomTokenStore.readRefreshToken tokenValue={}", tokenValue); Oauthtoken oauthtoken = oauthtokenRepository.findByRefreshid(DigestUtils.md5Hex(tokenValue)); if (oauthtoken.getRefreshed() == Boolean.TRUE) { throw new BadCredentialsException("RefreshToken Is Refreshed."); } OAuth2RefreshToken oAuth2RefreshToken = new DefaultOAuth2RefreshToken(oauthtoken.getRefreshtoken()); log.debug("<< CustomTokenStore.readRefreshToken OAuth2RefreshToken={}", oAuth2RefreshToken); return oAuth2RefreshToken; } /** * 讀取當初的授權資料才能再核發 Token * * @param token * @return */ @Override public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) { log.debug(">> CustomTokenStore.readAuthenticationForRefreshToken token={}", token); OAuth2Authentication oAuth2Authentication = null; Oauthtoken oauthtoken = oauthtokenRepository.findByRefreshid(DigestUtils.md5Hex(token.getValue())); try { ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(oauthtoken.getAuthentication())); oAuth2Authentication = (OAuth2Authentication) ois.readObject(); } catch (Exception e) { log.error("OAuth2Authentication Deserialization error", e); } log.debug("<< CustomTokenStore.readAuthenticationForRefreshToken oAuth2Authentication={}", oAuth2Authentication); return oAuth2Authentication; } @Override public void removeRefreshToken(OAuth2RefreshToken token) { log.debug(">> CustomTokenStore.removeRefreshToken token={}", token); } /** * 當授權成功後回頭把 refreshToken 標記為已更新核發過 * * @param refreshToken */ @Override public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { log.debug(">> CustomTokenStore.removeAccessTokenUsingRefreshToken refreshToken={}", refreshToken); Oauthtoken oauthtoken = oauthtokenRepository.findByRefreshid(DigestUtils.md5Hex(refreshToken.getValue())); oauthtoken.setRefreshed(Boolean.TRUE); oauthtokenRepository.save(oauthtoken); log.debug("<< CustomTokenStore.removeAccessTokenUsingRefreshToken"); } @Override public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { log.debug(">> CustomTokenStore.getAccessToken authentication={}", authentication); return null; } @Override public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) { log.debug(">> CustomTokenStore.findTokensByClientIdAndUserName clientId={}, userName={}", clientId, userName); log.debug("<< CustomTokenStore.findTokensByClientIdAndUserName Collection={}", "[]"); return Collections.emptySet(); } @Override public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) { log.debug(">> CustomTokenStore.findTokensByClientId clientId={}", clientId); return null; } }
實作 UserDetailsService
這是介面提供 security 來讀取用戶資料
package com.ps.security; import com.ps.model.Account; import com.ps.model.Role; import com.ps.repository.AccountRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Collection; /** * Created by samchu on 2017/2/15. */ @Slf4j @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private AccountRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { log.debug(">> CustomUserDetailsService.loadUserByUsername username={}", username); Account account = userRepository.findByUsername(username); if (account == null) { // Not found... throw new UsernameNotFoundException( "User " + username + " not found."); } if (account.getRoles() == null || account.getRoles().isEmpty()) { // No Roles assigned to user... throw new UsernameNotFoundException("User not authorized."); } Collection<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>(); for (Role role : account.getRoles()) { grantedAuthorities.add(new SimpleGrantedAuthority(role.getCode())); } User userDetails = new User(account.getUsername(), account.getPassword(), account.isEnabled(), !account.isExpired(), !account.isCredentialsexpired(), !account.isLocked(), grantedAuthorities); log.debug("<< CustomUserDetailsService.loadUserByUsername User={}", userDetails); return userDetails; } }
繼承 AbstractUserDetailsAuthenticationProvider.java
繼承 org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider
這支是在驗證用戶帳密,我們使用 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 來做密碼的儲存 相關範例請參考 Spring BCryptPasswordEncoder
BCryptPasswordEncoder 是 spring security 3 推薦的
安全性更多閱讀 在我的印象中,hash+salt已经足够好了。为什么我还要使用BCrypt?
package com.ps.security; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * Created by samchu on 2017/2/15. */ @Slf4j @Component public class CustomUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { @Autowired private CustomUserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { log.debug(">> CustomUserDetailsAuthenticationProvider.additionalAuthenticationChecks userDetails={}, authentication={}", userDetails, authentication); if (authentication.getCredentials() == null || userDetails.getPassword() == null) { throw new BadCredentialsException("Credentials may not be null."); } if (!passwordEncoder.matches((String) authentication.getCredentials(), userDetails.getPassword())) { throw new BadCredentialsException("Invalid credentials."); } log.debug("<< CustomUserDetailsAuthenticationProvider.additionalAuthenticationChecks"); } @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { log.debug(">> CustomUserDetailsAuthenticationProvider.retrieveUser username={}, authentication={}", username, authentication); UserDetails userDetails = userDetailsService.loadUserByUsername(username); log.debug("< CustomUserDetailsAuthenticationProvider.retrieveUser UserDetails={}", userDetails); return userDetails; } }
如果要客製化 AccessTokenConverter
最後 AccessTokenConverter 不一定需要實作 這個是把原本亂數產生 Token 的方式轉成 JWT 格式
原本的預設的在這 org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter
而我們這支是跟原本的一模一樣 只是方便我們想去加些什麼在 JWT 內
package com.ps.security; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.security.oauth2.provider.token.AccessTokenConverter; import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter; import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter; import java.util.*; /** * 將 token 轉成 JWT 跟原來程式一模一樣 * https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/token/DefaultAccessTokenConverter.java * Created by samchu on 2017/2/15. */ @Slf4j public class CustomAccessTokenConverter implements AccessTokenConverter { private UserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter(); private boolean includeGrantType; /** * Converter for the part of the data in the token representing a user. * * @param userTokenConverter the userTokenConverter to set */ public void setUserTokenConverter(UserAuthenticationConverter userTokenConverter) { log.debug(">> CustomAccessTokenConverter.setUserTokenConverter UserAuthenticationConverter={}", userTokenConverter); this.userTokenConverter = userTokenConverter; } /** * Flag to indicate the the grant type should be included in the converted token. * * @param includeGrantType the flag value (default false) */ public void setIncludeGrantType(boolean includeGrantType) { log.debug(">> CustomAccessTokenConverter.setIncludeGrantType includeGrantType={}", includeGrantType); this.includeGrantType = includeGrantType; } public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { log.debug(">> CustomAccessTokenConverter.convertAccessToken token={}, authentication={}", token, authentication); Map<String, Object> response = new HashMap<String, Object>(); OAuth2Request clientToken = authentication.getOAuth2Request(); if (!authentication.isClientOnly()) { response.putAll(userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication())); } else { if (clientToken.getAuthorities() != null && !clientToken.getAuthorities().isEmpty()) { response.put(UserAuthenticationConverter.AUTHORITIES, AuthorityUtils.authorityListToSet(clientToken.getAuthorities())); } } if (token.getScope() != null) { response.put(SCOPE, token.getScope()); } if (token.getAdditionalInformation().containsKey(JTI)) { response.put(JTI, token.getAdditionalInformation().get(JTI)); } if (token.getExpiration() != null) { response.put(EXP, token.getExpiration().getTime() / 1000); } if (includeGrantType && authentication.getOAuth2Request().getGrantType() != null) { response.put(GRANT_TYPE, authentication.getOAuth2Request().getGrantType()); } response.putAll(token.getAdditionalInformation()); response.put(CLIENT_ID, clientToken.getClientId()); if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) { response.put(AUD, clientToken.getResourceIds()); } log.debug("<< CustomAccessTokenConverter.convertAccessToken response={}", response); return response; } public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) { log.debug(">> CustomAccessTokenConverter.extractAccessToken value={}, map={}", value, map); DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(value); Map<String, Object> info = new HashMap<String, Object>(map); info.remove(EXP); info.remove(AUD); info.remove(CLIENT_ID); info.remove(SCOPE); if (map.containsKey(EXP)) { token.setExpiration(new Date((Long) map.get(EXP) * 1000L)); } if (map.containsKey(JTI)) { info.put(JTI, map.get(JTI)); } token.setScope(extractScope(map)); token.setAdditionalInformation(info); log.debug("<< CustomAccessTokenConverter.extractAccessToken DefaultOAuth2AccessToken={}", token); return token; } public OAuth2Authentication extractAuthentication(Map<String, ?> map) { log.debug(">> CustomAccessTokenConverter.extractAuthentication map={}", map); Map<String, String> parameters = new HashMap<String, String>(); Set<String> scope = extractScope(map); Authentication user = userTokenConverter.extractAuthentication(map); String clientId = (String) map.get(CLIENT_ID); parameters.put(CLIENT_ID, clientId); if (includeGrantType && map.containsKey(GRANT_TYPE)) { parameters.put(GRANT_TYPE, (String) map.get(GRANT_TYPE)); } Set<String> resourceIds = new LinkedHashSet<String>(map.containsKey(AUD) ? getAudience(map) : Collections.<String>emptySet()); Collection<? extends GrantedAuthority> authorities = null; if (user == null && map.containsKey(AUTHORITIES)) { @SuppressWarnings("unchecked") String[] roles = ((Collection<String>) map.get(AUTHORITIES)).toArray(new String[0]); authorities = AuthorityUtils.createAuthorityList(roles); } OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null, null); OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(request, user); log.debug("<< CustomAccessTokenConverter.extractAuthentication OAuth2Authentication={}", oAuth2Authentication); return oAuth2Authentication; } private Collection<String> getAudience(Map<String, ?> map) { log.debug(">> CustomAccessTokenConverter.getAudience map={}", map); Object auds = map.get(AUD); if (auds instanceof Collection) { @SuppressWarnings("unchecked") Collection<String> result = (Collection<String>) auds; return result; } Collection<String> collection = Collections.singleton((String) auds); log.debug("<< CustomAccessTokenConverter.getAudience Collection={}", collection); return collection; } private Set<String> extractScope(Map<String, ?> map) { log.debug(">> CustomAccessTokenConverter.extractScope map={}", map); Set<String> scope = Collections.emptySet(); if (map.containsKey(SCOPE)) { Object scopeObj = map.get(SCOPE); if (String.class.isInstance(scopeObj)) { scope = new LinkedHashSet<String>(Arrays.asList(String.class.cast(scopeObj).split(" "))); } else if (Collection.class.isAssignableFrom(scopeObj.getClass())) { @SuppressWarnings("unchecked") Collection<String> scopeColl = (Collection<String>) scopeObj; scope = new LinkedHashSet<String>(scopeColl); // Preserve ordering } } log.debug("<< CustomAccessTokenConverter.extractScope scope={}", scope); return scope; } }
如果 JWT 內的 exp 時間直接解開來看起來很怪是沒有問題的喔,因為在轉換過程中有處理過,你用其他套件他也會換算回來的
if (token.getExpiration() != null) { response.put(EXP, token.getExpiration().getTime() / 1000); }
接下來 EnableWebSecurity
package com.ps.security; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; /** * Created by samchu on 2017/2/15. */ @Slf4j @Configuration @EnableWebSecurity public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsAuthenticationProvider customUserDetailsAuthenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { log.info(">> WebSecurityConfiguration.configure AuthenticationManagerBuilder={}", auth); auth.authenticationProvider(customUserDetailsAuthenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } @Bean public TokenStore tokenStore() { //JdbcTokenStore jdbcTokenStore = new JdbcTokenStore(dataSource); return new CustomTokenStore(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { final JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setSigningKey("ASDFASFsdfsdfsdfsfadsf234asdfasfdas"); // 註解掉的原因是因為跟原本的一樣,但記錄一下如果需要特別調整可以在這調 //jwtAccessTokenConverter.setAccessTokenConverter(new CustomAccessTokenConverter()); return jwtAccessTokenConverter; } }
配置我們 AuthorizationServer 並把我們服務組件組裝起來
package com.ps.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; 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.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; /** * Created by samchu on 2017/2/15. */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired private CustomUserDetailsService userDetailsService; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenStore tokenStore; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(tokenStore) .userDetailsService(userDetailsService) .authenticationManager(authenticationManager) .accessTokenConverter(jwtAccessTokenConverter); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients .inMemory() .withClient("clientapp") .authorizedGrantTypes("password", "refresh_token") .scopes("account", "account.readonly", "role", "role.readonly") .resourceIds("account") .secret("123456").accessTokenValiditySeconds(3600).refreshTokenValiditySeconds(3600) .and() .withClient("clientkpi") .authorizedGrantTypes("password", "refresh_token") .scopes("account", "account.readonly", "role", "role.readonly") .resourceIds("account", "kpi") .secret("123456").accessTokenValiditySeconds(3600).refreshTokenValiditySeconds(3600) .and() .withClient("web") .redirectUris("http://www.google.com.tw") .secret("123456") .authorizedGrantTypes("implicit") .scopes("account", "account.readonly", "role", "role.readonly") .resourceIds("friend", "common", "user") .accessTokenValiditySeconds(3600); //http://localhost:8080/oauth/authorize?response_type=token&client_id=web } }
怎麼設計 Scope 也許可以參考 https://developers.google.com/identity/protocols/googlescopes
Client 其實也可以配置到資料庫中,不過我們還沒對外開放,所以還不需要。
我們配置了兩個客戶端 clientapp 是走 password 可信任的內部服務
web 則是 implicit 外部一次性授權 網頁方式授權
忘記了就回上面看吧
啟動主程式
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableJpaAuditing //@EnableTransactionManagement @SpringBootApplication public class AuthApplication { public static void main(String[] args) { SpringApplication.run(AuthApplication.class, args); } }
測試
password Auth
Request
curl --request POST \ --url http://localhost:8080/oauth/token \ --header 'authorization: Basic Y2xpZW50YXBwOjEyMzQ1Ng==' \ --header 'cache-control: no-cache' \ --header 'content-type: application/x-www-form-urlencoded' \ --data 'username=papidakos&password=papidakos123&grant_type=password&scope=account%20role'
response
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE0ODcyMjIxNDMsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiIzMWUzYzdiNi0zY2U4LTQ1YWMtOGU1Mi1lNzU0M2JhZTljMzUiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.tUCo7NUhMCZDz_CMyr9fsVSqwFoHEvkSOfZHAeMEmn8", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiIzMWUzYzdiNi0zY2U4LTQ1YWMtOGU1Mi1lNzU0M2JhZTljMzUiLCJleHAiOjE0ODcyMjIxNDMsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiIxNDVhNjFkNi0wYzczLTQ4YzUtOWE0ZS1kNzNiNzI0MTY4YmYiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.zXdUTCdiXT5pOpjRanRkrGpiIG3p_C4AsiysjIWHtS8", "expires_in": 499, "scope": "read write", "jti": "31e3c7b6-3ce8-45ac-8e52-e7543bae9c35" }
password refresh
Request
curl --request POST \ --url http://localhost:8080/oauth/token \ --header 'authorization: Basic Y2xpZW50YXBwOjEyMzQ1Ng==' \ --header 'cache-control: no-cache' \ --header 'content-type: application/x-www-form-urlencoded' \ --header 'postman-token: f754a47d-f7b7-7ad7-c517-02969addfcbb' \ --data 'grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiI0ZTY5ZmJmZS00ODAzLTQ0YTYtOTBkOC1hOTcwMDY2YjhlZTEiLCJleHAiOjE0ODcyMTQxNTUsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiJkMTM2OTExNS04NTIwLTRlMDctYTUzNS0yNTA3NDM0OTAxZWIiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.WaHrDJa2mgZxjUDZ2WRsB7_bQluF2HkVk0ILct7KZRA'
response
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE0ODcyMTQxNjksImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiJjMzNjZGViMi00NjgyLTRkZTEtOWYwYy1kMWUyMGIxNzIyMDYiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.p7n8tOpAr6EKpdV47bo-re-qway2Zz59j0nj-4Fl-48", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiJjMzNjZGViMi00NjgyLTRkZTEtOWYwYy1kMWUyMGIxNzIyMDYiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiZDEzNjkxMTUtODUyMC00ZTA3LWE1MzUtMjUwNzQzNDkwMWViIiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIn0.HaMmBQY7BRlcvjEHt4CVn4j3G74luN_7ZaqssC1XPlY", "expires_in": 499, "scope": "read write", "jti": "c33cdeb2-4682-4de1-9f0c-d1e20b172206" }
implicit
使用瀏覽器開啟 http://localhost:8080/oauth/authorize?response_type=token&client_id=web
有點醜沒關係,這是可以客製的
再看一下原始碼這頁面是有擋 跨站請求偽造(Cross-site request forgery)
<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'> <h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'> <table> <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr> <tr><td>Password:</td><td><input type='password' name='password'/></td></tr> <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr> <input name="_csrf" type="hidden" value="2c8806fa-ee70-44dc-b289-5dbc0df07ed9" /> </table> </form></body></html>
輸入正確帳密之後後有個授權清單頁面
同意之後就會產生 Token 透過瀏覽器 轉回客戶端設定的 http://www.google.com.tw網址如下
https://www.google.com.tw/?gws_rd=ssl#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiY29tbW9uIiwiZnJpZW5kIiwidXNlciJdLCJ1c2VyX25hbWUiOiJwYXBpZGFrb3MiLCJzY29wZSI6WyJjb21tb24iLCJ1c2VyLnJlYWRvbmx5IiwiZnJpZW5kIl0sImV4cCI6MTQ4NzIyNzI1MSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjcxNjU3ZmNlLTdmNTktNDMwYi1hMjUzLTc5MmNiYzZjZmMyYSIsImNsaWVudF9pZCI6IndlYiJ9.xKktY90aizvAFaR7W1eJzn4NIQLuIaaG88lfTQzSNlQ&token_type=bearer&expires_in=3599&scope=common%20user.readonly%20friend&jti=71657fce-7f59-430b-a253-792cbc6cfc2a
AuthServer 這邊就已經可以用了
想簡單用可以走 implicit 想控制權高一點又可以 refresh 就用 password
Resource Server 則不一定需要套 Spring Security 你也可以簡單使用 Filter 、 LocalThread 、 JWT 套件 就可以達成
那些 x-xss-protection 再自己加上也蠻快的
等我寫到 Resource Server 再考慮要不要再寫一篇
以上所述就是小编给大家介绍的《use JWT OAuth2 and spring-security Create AuthorizationServer》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- use JWT OAuth2 and spring-security Create AuthorizationServer
- use JWT OAuth2 and spring-security Create AuthorizationServer
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
R for Data Science
Hadley Wickham、Garrett Grolemund / O'Reilly Media / 2016-12-25 / USD 39.99
http://r4ds.had.co.nz/一起来看看 《R for Data Science》 这本书的介绍吧!