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

重拾後端之Spring Boot(一):REST API的搭建能夠這樣簡單
重拾後端之Spring Boot(二):MongoDb的無縫集成
重拾後端之Spring Boot(三):找回熟悉的Controller,Service
重拾後端之Spring Boot(四):使用 JWT 和 Spring Security 保護 REST APIjavascript

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

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

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

什麼是JWT?

JWT是 Json Web Token 的縮寫。它是基於 RFC 7519 標準定義的一種能夠安全傳輸的 小巧自包含 的JSON對象。因爲數據是使用數字簽名的,因此是可信任的和安全的。JWT可使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。java

JWT的工做流程

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

  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生成後的樣子,下面那坨亂糟糟的就是了。github

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ.RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg複製代碼

但仔細看到的話仍是能夠看到這個token分紅了三部分,每部分用 . 分隔,每段都是用 Base64 編碼的。若是咱們用一個Base64的解碼器的話 ( www.base64decode.org/ ),能夠看到第一部分 eyJhbGciOiJIUzUxMiJ9 被解析成了: web

{
    "alg":"HS512"
}複製代碼

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

第二部分 eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ 被解碼以後是 spring

{
    "sub":"wang",
    "created":1489079981393,
    "exp":1489684781
}複製代碼

這段告訴咱們這個Token中含有的數據聲明(Claim),這個例子裏面有三個聲明:sub, createdexp。在咱們這個例子中,分別表明着用戶名、建立時間和過時時間,固然你能夠把任意數據聲明在這裏。mongodb

看到這裏,你可能會想這是個什麼鬼token,全部信息都透明啊,安全怎麼保障?別急,咱們看看token的第三段 RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg。一樣使用Base64解碼以後,咦,這是什麼東東

D X    DmYTeȧLUZcPZ0$gZAY_7wY@複製代碼

最後一段實際上是簽名,這個簽名必須知道祕鑰才能計算。這個也是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 ( 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的基礎內容或展開內容,請自行去官網學習( projects.spring.io/spring-secu… )。

簡單的背景知識

若是你的系統有用戶的概念的話,通常來講,你應該有一個用戶表,最簡單的用戶表,應該有三列: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

# Server configuration
server:
 port: 8090
 contextPath:

# Spring configuration
spring:
 jackson:
 serialization:
 INDENT_OUTPUT: true
  data.mongodb:
 host: localhost
 port: 27017
 database: springboot

# Logging configuration
logging:
 level:
    org.springframework:
 data: DEBUG
 security: DEBUG複製代碼

咱們除了配置了logging的一些東東外,也順手設置了數據庫和http服務的一些配置項,如今咱們的服務器會在8090端口監聽,而spring data和security的日誌在debug模式下會輸出到console。

如今啓動服務後,訪問 http://localhost:8090 你能夠看到根目錄仍是正常顯示的

根目錄仍是正常能夠訪問的

但咱們試一下 http://localhost:8090/users ,觀察一下console,咱們會看到以下的輸出,告訴因爲用戶未鑑權,咱們訪問被拒絕了。

2017-03-10 15:51:53.351 DEBUG 57599 --- [nio-8090-exec-4] o.s.s.w.a.ExceptionTranslationFilter     : Access is denied (user is anonymous); redirecting to authentication entry point

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]複製代碼

集成JWT和Spring Security

到如今,咱們仍是讓JWT和Spring Security各自爲戰,並無集成起來。要想要JWT在Spring中工做,咱們應該新建一個filter,並把它配置在 WebSecurityConfig 中。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(tokenHead)) {
            final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer "
            String username = jwtTokenUtil.getUsernameFromToken(authToken);

            logger.info("checking authentication " + username);

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                            request));
                    logger.info("authenticated user " + username + ", setting security context");
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }

        chain.doFilter(request, response);
    }
}複製代碼

事實上若是咱們足夠相信token中的數據,也就是咱們足夠相信簽名token的secret的機制足夠好,這種狀況下,咱們能夠不用再查詢數據庫,而直接採用token中的數據。本例中,咱們仍是經過Spring Security的 @UserDetailsService 進行了數據查詢,但簡單驗證的話,你能夠採用直接驗證token是否合法來避免昂貴的數據查詢。

接下來,咱們會在 WebSecurityConfig 中注入這個filter,而且配置到 HttpSecurity 中:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    // 省略其它部分

    @Bean
    public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtAuthenticationTokenFilter();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 省略以前寫的規則部分,具體看前面的代碼

        // 添加JWT filter
        httpSecurity
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
    }
}複製代碼

完成鑑權(登陸)、註冊和更新token的功能

到如今,咱們整個API其實已經在安全的保護下了,但咱們遇到一個問題:全部的API都安全了,但咱們尚未用戶啊,因此全部API都無法訪問。所以要提供一個註冊、登陸的API,這個API應該是能夠匿名訪問的。給它規劃的路徑呢,咱們前面其實在WebSecurityConfig中已經給出了,就是 /auth

首先須要一個AuthService,規定一下必選動做:

public interface AuthService {
    User register(User userToAdd);
    String login(String username, String password);
    String refresh(String oldToken);
}複製代碼

而後,實現這些必選動做,其實很是簡單:

  1. 登陸時要生成token,完成Spring Security認證,而後返回token給客戶端
  2. 註冊時將用戶密碼用BCrypt加密,寫入用戶角色,因爲是開放註冊,因此寫入角色系統控制,將其寫成 ROLE_USER
  3. 提供一個能夠刷新token的接口 refresh 用於取得新的token
@Service
public class AuthServiceImpl implements AuthService {

    private AuthenticationManager authenticationManager;
    private UserDetailsService userDetailsService;
    private JwtTokenUtil jwtTokenUtil;
    private UserRepository userRepository;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    public AuthServiceImpl( AuthenticationManager authenticationManager, UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, UserRepository userRepository) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
        this.userRepository = userRepository;
    }

    @Override
    public User register(User userToAdd) {
        final String username = userToAdd.getUsername();
        if(userRepository.findByUsername(username)!=null) {
            return null;
        }
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        final String rawPassword = userToAdd.getPassword();
        userToAdd.setPassword(encoder.encode(rawPassword));
        userToAdd.setLastPasswordResetDate(new Date());
        userToAdd.setRoles(asList("ROLE_USER"));
        return userRepository.insert(userToAdd);
    }

    @Override
    public String login(String username, String password) {
        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
        final Authentication authentication = authenticationManager.authenticate(upToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        final String token = jwtTokenUtil.generateToken(userDetails);
        return token;
    }

    @Override
    public String refresh(String oldToken) {
        final String token = oldToken.substring(tokenHead.length());
        String username = jwtTokenUtil.getUsernameFromToken(token);
        JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);
        if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())){
            return jwtTokenUtil.refreshToken(token);
        }
        return null;
    }
}複製代碼

而後創建AuthController就好,這個AuthController中咱們在其中使用了表達式綁定,好比 @Value("${jwt.header}")中的 jwt.header 實際上是定義在 applicaiton.yml 中的

# JWT
jwt:
 header: Authorization
 secret: mySecret
 expiration: 604800
 tokenHead: "Bearer "
 route:
 authentication:
 path: auth
 refresh: refresh
 register: "auth/register"複製代碼

一樣的 @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST) 中的 jwt.route.authentication.path 也是定義在上面的

@RestController
public class AuthController {
    @Value("${jwt.header}")
    private String tokenHeader;

    @Autowired
    private AuthService authService;

    @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(
            @RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException{
        final String token = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword());

        // Return the token
        return ResponseEntity.ok(new JwtAuthenticationResponse(token));
    }

    @RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)
    public ResponseEntity<?> refreshAndGetAuthenticationToken(
            HttpServletRequest request) throws AuthenticationException{
        String token = request.getHeader(tokenHeader);
        String refreshedToken = authService.refresh(token);
        if(refreshedToken == null) {
            return ResponseEntity.badRequest().body(null);
        } else {
            return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
        }
    }

    @RequestMapping(value = "${jwt.route.authentication.register}", method = RequestMethod.POST)
    public User register(@RequestBody User addedUser) throws AuthenticationException{
        return authService.register(addedUser);
    }
}複製代碼

驗證時間

接下來,咱們就能夠看看咱們的成果了,首先註冊一個用戶 peng2,很完美的註冊成功了

註冊用戶

而後在 /auth 中取得token,也很成功

取得token

不使用token時,訪問 /users 的結果,不出意料的失敗,提示未受權。

不使用token訪問users列表

使用token時,訪問 /users 的結果,雖然還是失敗,但此次提示訪問被拒絕,意思就是雖然你已經獲得了受權,但因爲你的會員級別還只是普卡會員,因此你的請求被拒絕。

image_1bas22va52vk1rj445fhm87k72a.png-156.9kB

接下來咱們訪問 /users/?username=peng2,居然能夠訪問啊

訪問本身的信息是容許的

這是因爲咱們爲這個方法定義的權限就是:擁有ADMIN角色或者是當前用戶自己。Spring Security真是很方便,很強大。

@PostAuthorize("returnObject.username == principal.username or hasRole('ROLE_ADMIN')")
    @RequestMapping(value = "/",method = RequestMethod.GET)
    public User getUserByUsername(@RequestParam(value="username") String username) {
        return repository.findByUsername(username);
    }複製代碼

本章代碼: github.com/wpcfan/spri…

重拾後端之Spring Boot(一):REST API的搭建能夠這樣簡單
重拾後端之Spring Boot(二):MongoDb的無縫集成
重拾後端之Spring Boot(三):找回熟悉的Controller,Service
重拾後端之Spring Boot(四):使用 JWT 和 Spring Security 保護 REST API

相關文章
相關標籤/搜索