1. 程式人生 > >重拾後端之Spring Boot(四):使用JWT和Spring Security保護REST API

重拾後端之Spring Boot(四):使用JWT和Spring Security保護REST API

通常情況下,把API直接暴露出去是風險很大的,不說別的,直接被機器攻擊就喝一壺的。那麼一般來說,對API要劃分出一定的許可權級別,然後做一個使用者的鑑權,依據鑑權結果給予使用者開放對應的API。目前,比較主流的方案有幾種:

  1. 使用者名稱和密碼鑑權,使用Session儲存使用者鑑權結果。
  2. 使用OAuth進行鑑權(其實OAuth也是一種基於Token的鑑權,只是沒有規定Token的生成方式)
  3. 自行採用Token進行鑑權

第一種就不介紹了,由於依賴Session來維護狀態,也不太適合移動時代,新的專案就不要採用了。第二種OAuth的方案和JWT都是基於Token的,但OAuth其實對於不做開放平臺的公司有些過於複雜。我們主要介紹第三種:JWT。

什麼是JWT?

JWT是 Json Web Token 的縮寫。它是基於 RFC 7519 標準定義的一種可以安全傳輸的 小巧 和 自包含 的JSON物件。由於資料是使用數字簽名的,所以是可信任的和安全的。JWT可以使用HMAC演算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。

JWT的工作流程

下面是一個JWT的工作流程圖。模擬一下實際的流程是這樣的(假設受保護的API在 /protected 中)

  1. 使用者導航到登入頁,輸入使用者名稱、密碼,進行登入
  2. 伺服器驗證登入鑑權,如果改使用者合法,根據使用者的資訊和伺服器的規則生成JWT Token
  3. 伺服器將該token以json形式返回(不一定要json形式,這裡說的是一種常見的做法)
  4. 使用者得到token,存在localStorage、cookie或其它資料儲存形式中。
  5. 以後使用者請求 /protected 中的API時,在請求的header中加入 Authorization: Bearer xxxx(token) 。此處注意token之前有一個7字元長度的 Bearer
  6. 伺服器端對此token進行檢驗,如果合法就解析其中內容,根據其擁有的許可權和自己的業務邏輯給出對應的響應結果。
  7. 使用者取得結果
JWT工作流程圖

為了更好的理解這個token是什麼,我們先來看一個token生成後的樣子,下面那坨亂糟糟的就是了。

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ
.RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg

但仔細看到的話還是可以看到這個token分成了三部分,每部分用 . 分隔,每段都是用 Base64編碼的。如果我們用一個Base64的解碼器的話 ( https://www.base64decode.org/ ),可以看到第一部分 eyJhbGciOiJIUzUxMiJ9 被解析成了:

{
    "alg":"HS512"
}

這是告訴我們HMAC採用HS512演算法對JWT進行的簽名。

第二部分 eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ 被解碼之後是

{
    "sub":"wang",
    "created":1489079981393,
    "exp":1489684781
}

這段告訴我們這個Token中含有的資料宣告(Claim),這個例子裡面有三個宣告: sub , created 和 exp 。在我們這個例子中,分別代表著使用者名稱、建立時間和過期時間,當然你可以把任意資料宣告在這裡。

看到這裡,你可能會想這是個什麼鬼token,所有資訊都透明啊,安全怎麼保障?別急,我們看看token的第三段 RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg 。同樣使用Base64解碼之後,咦,這是什麼東東

D X    DmYTeȧLUZcPZ0$gZAY[email protected]

最後一段其實是簽名,這個簽名必須知道祕鑰才能計算。這個也是JWT的安全保障。這裡提一點注意事項,由於資料宣告(Claim)是公開的,千萬不要把密碼等敏感欄位放進去,否則就等於是公開給別人了。

也就是說JWT是由三段組成的,按官方的叫法分別是header(頭)、payload(負載)和signature(簽名):

header.payload.signature

頭中的資料通常包含兩部分:一個是我們剛剛看到的 alg ,這個詞是 algorithm 的縮寫,就是指明演算法。另一個可以新增的欄位是token的型別(按RFC 7519實現的token機制不只JWT一種),但如果我們採用的是JWT的話,指定這個就多餘了。

{
  "alg": "HS512",
  "typ": "JWT"
}

payload中可以放置三類資料:系統保留的、公共的和私有的:

  • 系統保留的宣告(Reserved claims):這類宣告不是必須的,但是是建議使用的,包括:iss (簽發者), exp (過期時間), 
    sub (主題), aud (目標受眾)等。這裡我們發現都用的縮寫的三個字元,這是由於JWT的目標就是儘可能小巧。
  • 公共宣告:這類宣告需要在 IANA JSON Web Token Registry 中定義或者提供一個URI,因為要避免重名等衝突。
  • 私有宣告:這個就是你根據業務需要自己定義的資料了。

簽名的過程是這樣的:採用header中宣告的演算法,接受三個引數:base64編碼的header、base64編碼的payload和祕鑰(secret)進行運算。簽名這一部分如果你願意的話,可以採用RSASHA256的方式進行公鑰、私鑰對的方式進行,如果安全性要求的高的話。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

JWT的生成和解析

為了簡化我們的工作,這裡引入一個比較成熟的JWT類庫,叫 jjwt ( https://github.com/jwtk/jjwt )。這個類庫可以用於Java和Android的JWT token的生成和驗證。

JWT的生成可以使用下面這樣的程式碼完成:

String generateToken(Map<String, Object> claims) {
    return Jwts.builder()
            .setClaims(claims)
            .setExpiration(generateExpirationDate())
            .signWith(SignatureAlgorithm.HS512, secret) //採用什麼演算法是可以自己選擇的,不一定非要採用HS512
            .compact();
}

資料宣告(Claim)其實就是一個Map,比如我們想放入使用者名稱,可以簡單的建立一個Map然後put進去就可以了。

Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, username());

解析也很簡單,利用 jjwt 提供的parser傳入祕鑰,然後就可以解析token了。

Claims getClaimsFromToken(String token) {
    Claims claims;
    try {
        claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    } catch (Exception e) {
        claims = null;
    }
    return claims;
}

JWT本身沒啥難度,但安全整體是一個比較複雜的事情,JWT只不過提供了一種基於token的請求驗證機制。但我們的使用者許可權,對於API的許可權劃分、資源的許可權劃分,使用者的驗證等等都不是JWT負責的。也就是說,請求驗證後,你是否有許可權看對應的內容是由你的使用者角色決定的。所以我們這裡要利用Spring的一個子專案Spring Security來簡化我們的工作。

Spring Security

Spring Security是一個基於Spring的通用安全框架,裡面內容太多了,本文的主要目的也不是展開講這個框架,而是如何利用Spring Security和JWT一起來完成API保護。所以關於Spring Secruity的基礎內容或展開內容,請自行去官網學習( http://projects.spring.io/spring-security/ )。

簡單的背景知識

如果你的系統有使用者的概念的話,一般來說,你應該有一個使用者表,最簡單的使用者表,應該有三列:Id,Username和Password,類似下表這種

ID USERNAME PASSWORD
10 wang abcdefg

而且不是所有使用者都是一種角色,比如網站管理員、供應商、財務等等,這些角色和網站的直接使用者需要的許可權可能是不一樣的。那麼我們就需要一個角色表:

ID ROLE
10 USER
20 ADMIN

當然我們還需要一個可以將使用者和角色關聯起來建立對映關係的表。

USER_ID ROLE_ID
10 10
20 20

這是典型的一個關係型資料庫的使用者角色的設計,由於我們要使用的MongoDB是一個文件型資料庫,所以讓我們重新審視一下這個結構。

這個資料結構的優點在於它避免了資料的冗餘,每個表負責自己的資料,通過關聯表進行關係的描述,同時也保證的資料的完整性:比如當你修改角色名稱後,沒有髒資料的產生。

但是這種事情在使用者許可權這個領域發生的頻率到底有多少呢?有多少人每天不停的改的角色名稱?當然如果你的業務場景確實是需要保證資料完整性,你還是應該使用關係型資料庫。但如果沒有高頻的對於角色表的改動,其實我們是不需要這樣的一個設計的。在MongoDB中我們可以將其簡化為

{
  _id: <id_generated>
  username: 'user',
  password: 'pass',
  roles: ['USER', 'ADMIN']
}

基於以上考慮,我們重構一下 User 類,

@Data
public class User {
    @Id
    private String id;

    @Indexed(unique=true, direction= IndexDirection.DESCENDING, dropDups=true)
    private String username;

    private String password;
    private String email;
    private Date lastPasswordResetDate;
    private List<String> roles;
}

當然你可能發現這個類有點怪,只有一些field,這個簡化的能力是一個叫 lombok 類庫提供的 ,這個很多開發過Android的童鞋應該熟悉,是用來簡化POJO的建立的一個類庫。簡單說一下,採用 lombok 提供的 @Data 修飾符後可以簡寫成,原來的一坨getter和setter以及constructor等都不需要寫了。類似的 Todo 可以改寫成:

@Data
public class Todo {
    @Id private String id;
    private String desc;
    private boolean completed;
    private User user;
}

增加這個類庫只需在 build.gradle 中增加下面這行

dependencies {
    // 省略其它依賴
    compile("org.projectlombok:lombok:${lombokVersion}")
}

引入Spring Security

要在Spring Boot中引入Spring Security非常簡單,修改 build.gradle ,增加一個引用 org.springframework.boot:spring-boot-starter-security :

dependencies {
    compile("org.springframework.boot:spring-boot-starter-data-rest")
    compile("org.springframework.boot:spring-boot-starter-data-mongodb")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("io.jsonwebtoken:jjwt:${jjwtVersion}")
    compile("org.projectlombok:lombok:${lombokVersion}")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

你可能發現了,我們不只增加了對Spring Security的編譯依賴,還增加 jjwt 的依賴。

Spring Security需要我們實現幾個東西,第一個是UserDetails:這個介面中規定了使用者的幾個必須要有的方法,所以我們建立一個JwtUser類來實現這個介面。為什麼不直接使用User類?因為這個UserDetails完全是為了安全服務的,它和我們的領域類可能有部分屬性重疊,但很多的介面其實是安全定製的,所以最好新建一個類:

public class JwtUser implements UserDetails {
    private final String id;
    private final String username;
    private final String password;
    private final String email;
    private final Collection<? extends GrantedAuthority> authorities;
    private final Date lastPasswordResetDate;

    public JwtUser(
            String id,
            String username,
            String password,
            String email,
            Collection<? extends GrantedAuthority> authorities,
            Date lastPasswordResetDate) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.authorities = authorities;
        this.lastPasswordResetDate = lastPasswordResetDate;
    }
    //返回分配給使用者的角色列表
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @JsonIgnore
    public String getId() {
        return id;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }
    // 賬戶是否未過期
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 賬戶是否未鎖定
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 密碼是否未過期
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    // 賬戶是否啟用
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
    // 這個是自定義的,返回上次密碼重置日期
    @JsonIgnore
    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }
}

這個介面中規定的很多方法我們都簡單粗暴的設成直接返回某個值了,這是為了簡單起見,你在實際開發環境中還是要根據具體業務調整。當然由於兩個類還是有一定關係的,為了寫起來簡單,我們寫一個工廠類來由領域物件建立 JwtUser ,這個工廠就叫 JwtUserFactory 吧:

public final class JwtUserFactory {

    private JwtUserFactory() {
    }

    public static JwtUser create(User user) {
        return new JwtUser(
                user.getId(),
                user.getUsername(),
                user.getPassword(),
                user.getEmail(),
                mapToGrantedAuthorities(user.getRoles()),
                user.getLastPasswordResetDate()
        );
    }

    private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

第二個要實現的是 UserDetailsService ,這個介面只定義了一個方法 loadUserByUsername ,顧名思義,就是提供一種從使用者名稱可以查到使用者並返回的方法。注意,不一定是資料庫哦,文字檔案、xml檔案等等都可能成為資料來源,這也是為什麼Spring提供這樣一個介面的原因:保證你可以採用靈活的資料來源。接下來我們建立一個 JwtUserDetailsServiceImpl 來實現這個介面。

@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);

        if (user == null) {
            throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
        } else {
            return JwtUserFactory.create(user);
        }
    }
}

為了讓Spring可以知道我們想怎樣控制安全性,我們還需要建立一個安全配置類 WebSecurityConfig :

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    // Spring會自動尋找同樣型別的具體類注入,這裡就是JwtUserDetailsServiceImpl了
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                // 設定UserDetailsService
                .userDetailsService(this.userDetailsService)
                // 使用BCrypt進行密碼的hash
                .passwordEncoder(passwordEncoder());
    }
    // 裝載BCrypt密碼編碼器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 由於使用的是JWT,我們這裡不需要csrf
                .csrf().disable()

                // 基於token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

                .authorizeRequests()
                //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

                // 允許對於網站靜態資源的無授權訪問
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).permitAll()
                // 對於獲取token的rest api要允許匿名訪問
                .antMatchers("/auth/**").permitAll()
                // 除上面外的所有請求全部需要鑑權認證
                .anyRequest().authenticated();

        // 禁用快取
        httpSecurity.headers().cacheControl();
    }
}

接下來我們要規定一下哪些資源需要什麼樣的角色可以訪問了,在 UserController 加一個修飾符 @PreAuthorize("hasRole('ADMIN')") 表示這個資源只能被擁有 ADMIN 角色的使用者訪問。

/**
 * 在 @PreAuthorize 中我們可以利用內建的 SPEL 表示式:比如 'hasRole()' 來決定哪些使用者有權訪問。
 * 需注意的一點是 hasRole 表示式認為每個角色名字前都有一個字首 'ROLE_'。所以這裡的 'ADMIN' 其實在
 * 資料庫中儲存的是 'ROLE_ADMIN' 。這個 @PreAuthorize 可以修飾Controller也可修飾Controller中的方法。
 **/
@RestController
@RequestMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public class UserController {
    @Autowired
    private UserRepository repository;

    @RequestMapping(method = RequestMethod.GET)
    public List<User> getUsers() {
        return repository.findAll();
    }

    // 略去其它部分
}

類似的我們給 TodoController 加上 @PreAuthorize("hasRole('USER')") ,標明這個資源只能被擁有 USER 角色的使用者訪問:

@RestController
@RequestMapping("/todos")
@PreAuthorize("hasRole('USER')")
public class TodoController {
    // 略去
}

使用application.yml配置SpringBoot應用

現在應該Spring Security可以工作了,但為了可以更清晰的看到工作日誌,我們希望配置一下,在和 src 同級建立一個config資料夾,在這個資料夾下面新建一個 application.yml 。

            
           

相關推薦

Spring Boot使用JWTSpring Security保護REST API

通常情況下,把API直接暴露出去是風險很大的,不說別的,直接被機器攻擊就喝一壺的。那麼一般來說,對API要劃分出一定的許可權級別,然後做一個使用者的鑑權,依據鑑權結果給予使用者開放對應的API。目前,比較主流的方案有幾種: 使用者名稱和密碼鑑權,使用Session儲存使用

Spring Boot Redis日誌

接著上篇內容繼續往下執行。 首先pom.xml 新增redis的引用,因為開始建專案沒有,自動生成redis的引用,然後手動新增進去。 新增成功以後看我們的配置檔案 新增我們的日誌類和快取 快取方法 結果: http://localhost:8080/set?key=lxh2&

Spring Boot Druid 連線池密碼加密與監控

在上一篇文章《Spring Boot (三): ORM 框架 JPA 與連線池 Hikari》 我們介紹了 JPA 與連線池 Hikari 的整合使用,在國內使用比較多的連線池還有一個是阿里開源的 Druid 。本篇文章我們就來聊一聊 Druid 的一些使用姿勢。 1. Druid 是什麼? 我們先來

Spring BootREST API的搭建可以這樣簡單

Spring Boot 話說我當年接觸Spring的時候著實興奮了好一陣,IoC的概念當初第一次聽說,感覺有種開天眼的感覺。記得當時的web框架和如今的前端框架的局面差不多啊,都是群雄紛爭。但一晃好多年沒寫過後端,程式碼這東西最怕手生,所以當作重新學習了,順便寫個學習筆記。 Spring Boot是什麼?

spring boot 熱部署

pom.xml文件 添加 gin 字節 loader 信息 dev spring tool 介紹了Spring boot實現熱部署的兩種方式,這兩種方法分別是使用 Spring Loaded和使用spring-boot-devtools進行熱部署。 熱部署是什麽

spring boot事務與緩存

autowire manager 控制 nsa color 實體 value ron save spring boot事務機制 spring支持聲明式事務,用@Tracsational註解在方法上表明該方法需要事務支持。被註解的方法在被調用時開啟一個新的事務,當方法無異常結

Spring基礎快速入門spring boot7spring boot 2.0簡單介紹

從這篇文章開始以spring boot2為主要版本進行使用介紹。 Spring boot 2特性 spring boot2在如下的部分有所變化和增強,相關特性在後續逐步展開。 特性增強 基礎元件升級: JDK1.8+ tomcat 8+ Thymeleaf 3

Spring Boot——Spring Data JPA

一、Spring Data簡介 Spring Data 專案的目的是為了簡化構建基於 Spring 框架應用的資料訪問技術,包括非關係資料庫、 Map-Reduce 框架、雲資料服務等等;另外也包含對關係資料庫的訪問支援。 Spring Data 包含多個子專案: S

Spring基礎快速入門spring boot4使用slf4j輸出日誌

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Spring基礎快速入門spring boot2SPRING INITIALIZR

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Spring Boot開啟宣告式事務

簡介 以前用Spring想要用事務的時候,都需要自己在spring的配置檔案中配置事務管理器。而Spring Boot則預設對jpa,jdbc,mybatis開啟了事務,引入他們的依賴的時候,事務就開啟了。使用事務只需要一個@Transactional註解就可以了。 準備 以上一篇文章【

Spring Boot Hello World(基礎篇)

我用的Eclipse 裝的springboot 的外掛,有的Eclipse 不支援springboot 換個新的就好了。據說IDE支援比較好,但是本人用習慣了Eclipse了,所有裝了一個Eclipse-photon版本,Eclipse的安裝就不介紹了,大家自行百度學習一下吧。 1.Ec

Spring基礎快速入門spring boot10spring boot + sonarqube +jacoco

上篇文章我們瞭解到瞭如何使用SonarQube對建立的SpringBoot的應用進行分析,這篇文章來接著確認一些如何視覺化地確認測試覆蓋率。 SpringBootTest 需要測試覆蓋率,自然,在此之前需要有測試用例,在前面的例子中已經簡單講述了在SpringBoot應用中進行

Spring基礎快速入門spring boot9使用sonarqube來檢查技術債務

作為程式碼質量檢查的流行工具,比如Sonarqube能夠檢查程式碼的“七宗罪”,跟程式碼結合起來能夠更好地提高程式碼的質量,讓我們來看一下,剛剛寫的Springboot2的HelloWorld的程式碼有什麼“罪”。 Sonarqube Sonarqube可以使用docker

Spring基礎快速入門spring boot8使用Junit進行測試

使用Junit或者TestNG可以進行單體測試,這篇文章簡單說明一下如何在Spring boot的專案中使用Junit進行單體測試。 pom設定 pom中需要新增spring-boot-starter-test <dependency> <g

Spring Boot整合Redis使用Redis實現快取共享

Redis(REmote DIctionary Server)是一個key-value儲存系統,是當下網際網路公司最常用的NoSQL資料庫之一。支援儲存的value型別有string、list、set、zset(sorted set --有序集合)和hash。Redis的資料

Spring boot3Spring boot中Redis 的使用

Spring boot除了常用的資料庫支援外,對nosql資料庫也進行了封裝自動化。 1 Redis介紹 Redis 是目前業界使用最廣泛的記憶體資料儲存。相比memcached, (1)Redis支援更豐富的資料結構,例如hashes,lists,sets等

Spring Boot概述1——起源、歷史、背景等

版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/lsxf_xin/article/details/79448037 概述:         Spring Boot為開發者帶來了更好的開發體驗,但寫完程式碼只是萬里長征路上的

Spring bootSpring boot+ mybatis 多資料來源最簡解決方案

多資料來源一般解決哪些問題?主從模式或者業務比較複雜需要連線不同的分庫來支援業務。 直接上程式碼。 配置檔案 pom包依賴,該依賴的依賴。主要是資料庫這邊的配置: mybatis.config-locations=classpath:mybatis/mybati

spring boot熱部署

熱部署作用: 在修改程式碼後無需重啟專案即可生效,提高開發效率。 部署方法如下: 首先,在pom.xml中引入依賴 <!-- 熱啟動 --> <dependency> <groupId>org.springframework.bo