use JWT OAuth2 and spring-security Create AuthorizationServer

栏目: 后端 · 发布时间: 6年前

内容简介: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 取得授權後才能繼續玩

use JWT OAuth2 and spring-security Create AuthorizationServer

關於 Implicit Grant Flow 注意幾點

Authorization Server 直接向 Client 核發 Access Token (一步)。

適合非常特定的 Public Clients ,例如跑在 Browser 裡面的應用程式。

Authorization Server 不必(也無法)驗證 Client 的身份。

禁止核發 Refresh Token。

需要 User-Agent Redirection。

有資料外洩風險

Resource Owner Credentials Grant Flow

是比較會偏內部可信任的應用在取得授權,因為會經手用戶的帳號密碼

use JWT OAuth2 and spring-security Create AuthorizationServer

關於 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);
use JWT OAuth2 and spring-security Create AuthorizationServer

專案

build.gradle
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

所以我們實作以上幾個動作就可以了

CustomTokenStore.java
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

介面 UserDetailsService.java

這是介面提供 security 來讀取用戶資料

CustomUserDetailsService.java
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?

CustomUserDetailsAuthenticationProvider.java
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 內

CustomAccessTokenConverter.java
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

WebSecurityConfiguration.java
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 並把我們服務組件組裝起來

AuthorizationServerConfiguration.java
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 外部一次性授權 網頁方式授權

忘記了就回上面看吧

啟動主程式

AuthApplication.java
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

use JWT OAuth2 and spring-security Create AuthorizationServer

有點醜沒關係,這是可以客製的

再看一下原始碼這頁面是有擋 跨站請求偽造(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>

輸入正確帳密之後後有個授權清單頁面

use JWT OAuth2 and spring-security Create AuthorizationServer
同意之後就會產生 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 再考慮要不要再寫一篇

← 關於注入的方式


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算机程序设计艺术(第1卷)

计算机程序设计艺术(第1卷)

[美] Donald E. Knuth / 清华大学出版社 / 2002-9 / 80.00元

第1卷首先介绍编程的基本概念和技术,然后详细讲解信息结构方面的内容,包括信息在计算机内部的表示方法、数据元素之间的结构关系,以及有效的信息处理方法。此外,书中还描述了编程在模拟、数值方法、符号计算、软件与系统设计等方面的初级应用。此第3版增加了数十项简单但重要的算法和技术,并根据当前研究发展趋势在数学预备知识方面做了大量修改。一起来看看 《计算机程序设计艺术(第1卷)》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

SHA 加密
SHA 加密

SHA 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具