(譯)Spring Boot + Spring Security + JWT + MySQL + React Full Stack Polling app - Part 2

原文鏈接: https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-2/

歡迎來(lái)到全棧開發(fā)系列第二章(Spring Boot厚骗,Spring Security个少,JWT坝疼,MySQL牙言,React)琢锋。
在第一章,我們創(chuàng)建了基礎(chǔ)的領(lǐng)域模型和數(shù)據(jù)倉(cāng)庫(kù)并啟動(dòng)了項(xiàng)目。
在本文中,我們將通過結(jié)合Spring Security和JWT來(lái)配置認(rèn)證功能皱碘,編寫用戶注冊(cè),登錄的API隐孽。

本項(xiàng)目的完整源碼托管在Github癌椿,如果你碰到困難,可隨時(shí)參考菱阵。

spring-boot-spring-security-jwt-login-signup-apis.jpg

安全機(jī)制概述

  • 構(gòu)建一個(gè)新用戶注冊(cè)的API踢俄,信息有 name, username, email , password
  • 構(gòu)建一個(gè)讓用戶通過 username或email和password登錄的API。在驗(yàn)證完用戶憑證后晴及,API應(yīng)該生成并返回一個(gè)JWT身份授權(quán)令牌
    客戶端請(qǐng)求受保護(hù)資源時(shí)應(yīng)該在每個(gè)request的頭部在Authorization中放入JWT Token都办。
  • 配置Spring Security限制受保護(hù)資源的訪問。比如
    • 登錄虑稼,注冊(cè)接口以及其他的靜態(tài)資源(圖片琳钉,scripts,css)應(yīng)該被通過
    • 創(chuàng)建調(diào)查蛛倦,發(fā)起投票等接口應(yīng)只能被認(rèn)證通過的用戶訪問歌懒。
  • 配置Spring Security,如果用戶未攜帶JWT Token訪問受保護(hù)資源溯壶,則拋出401未認(rèn)證通過錯(cuò)誤
  • 配置基于 角色 的授權(quán)及皂。比如
    • 只有角色是ADMIN的用戶可以創(chuàng)建一個(gè)調(diào)查
    • 只有角色是USER的用戶可以投票

配置 Spring Security 和 JWT

下面的類是實(shí)現(xiàn)安全的重中之重甫男,他包含了幾乎所有本項(xiàng)目需要的安全相關(guān)的配置。
讓我們首先在com.example.polls.config包下創(chuàng)建SecurityConfig验烧,然后我們照著代碼學(xué)習(xí)每個(gè)配置到底做了什么事查剖。

package com.example.polls.config;

import com.example.polls.security.CustomUserDetailsService;
import com.example.polls.security.JwtAuthenticationEntryPoint;
import com.example.polls.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    CustomUserDetailsService customUserDetailsService;

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                    .and()
                .csrf()
                    .disable()
                .exceptionHandling()
                    .authenticationEntryPoint(unauthorizedHandler)
                    .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .authorizeRequests()
                    .antMatchers("/",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                        .permitAll()
                    .antMatchers("/api/auth/**")
                        .permitAll()
                    .antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
                        .permitAll()
                    .antMatchers(HttpMethod.GET, "/api/polls/**", "/api/users/**")
                        .permitAll()
                    .anyRequest()
                        .authenticated();

        // Add our custom JWT security filter
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

    }
}

以上的 SecurityConfig類在你的IDE中會(huì)有幾處編譯錯(cuò)誤,因?yàn)槲覀冞€沒有定義此類中需要用到的一些類噪窘。我們將在文章的后文中定義他們笋庄。
但在此之前,讓我們理解這些注解的意義和代碼中相關(guān)的配置的含義倔监。

1. @EnableWebSecurity

這是Spring Sercurity主要的注解直砂,用于在項(xiàng)目中開啟Web Security。

2. @EnableGlobalMethodSecurity

這個(gè)注解用于開啟方法級(jí)別的安全浩习,你可使用以下3個(gè)類型的注解去保護(hù)你的方法静暂。

  • securityEnable: 它的作用是使 @Secured注解可以保護(hù)你的Controller/Service層方法。
@Secured("ROLE_ADMIN")
public User getAllUsers() {}

@Secured({"ROLE_USER", "ROLE_ADMIN"})
public User getUser(Long id) {}

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public boolean isUsernameAvailable() {}
  • jsr250Enabled:它可以讓@RolesAllowed可以像這樣使用
@RolesAllowed("ROLE_ADMIN")
public Poll createPoll() {}  
  • ** prePostEnabled:**它可以使用@PreAuthorize和@PostAuthorize注解谱秽,基于表達(dá)式構(gòu)造更復(fù)雜的訪問控制語(yǔ)法
@PreAuthorize("isAnonymous()")
public boolean isUsernameAvailable() {}

@PreAuthorize("hasRole('USER')")
public Poll createPoll() {}

3. WebSecurityConfigurerAdapter

此類實(shí)現(xiàn)了Spring Security的 WebSecurityConfigurer接口霉赡。它提供了默認(rèn)的安全配置項(xiàng),如需自定義一些自己的需求可以通過繼承他覆蓋他的方法來(lái)更改供搀。

我們的Security類繼承了WebSecurityConfigurerAdapter并復(fù)寫了他的幾個(gè)方法來(lái)提供自己的安全配置甸陌。

4. CustomUserDetailsService

為驗(yàn)證用戶和實(shí)現(xiàn)各種基于角色的檢查,Spring Security需要我們提供用戶的一些信息近哟。
因此驮审,他存在一個(gè)名為UserDetailService的接口,內(nèi)容為通過username檢索返回User相關(guān)信息吉执。

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

我們定義了CustomUserDetailsService實(shí)現(xiàn)了UserDetailsService接口并且提供了一個(gè)loadUserByUsername的具體實(shí)現(xiàn)疯淫。
注意,loadUserByUsername方法返回一個(gè)UserDetails對(duì)象戳玫,而Spring Security正需要他用于進(jìn)行各種認(rèn)證與基于角色的驗(yàn)證熙掺。
在我們的實(shí)現(xiàn)中,我們還定義了一個(gè)定制的UserPrincipal類實(shí)現(xiàn)了UserDetails接口咕宿,在loadUserByUsername()方法中我們返回了UserPrincipal對(duì)象币绩。(loadUserByUsername()方法返回的是UserDetails對(duì)象,作者是因?yàn)?code>UserPrincipal實(shí)現(xiàn)了UserDetails接口荠列,所以這么說也沒問題)

5. JwtAuthenticationEntryPoint

當(dāng)客戶端想訪問受保護(hù)資源类浪,缺沒有提供合適的認(rèn)證令牌時(shí),這個(gè)類返回401未認(rèn)證通過錯(cuò)誤給客戶端肌似。這個(gè)類實(shí)現(xiàn)了Spring Security的AuthenticationEntryPoint接口费就。

6. JwtAuthenticationFilter

我們用JwtAuthenticationFilter實(shí)現(xiàn)了過濾器的功能

  • 從requests中Header里的Authorization中讀取JWT授權(quán)信息
  • 驗(yàn)證token
  • 用戶信息與token關(guān)聯(lián)
  • 把用戶信息放到Spring Security的上下文(SecurityContext)中,Spring Security使用用戶信息去做一些校驗(yàn)川队。我們也可以從SecurityContext中取出用戶信息用在自己的業(yè)務(wù)邏輯中力细。

7.AuthenticationManagerBuilder 和 AuthenticationManager

AuthenticationManagerBuilder用于創(chuàng)建AuthenticationManager實(shí)例睬澡,這個(gè)接口就是Spring Security用于認(rèn)證用戶的主要接口。
你可以使用AuthenticationManagerBuilder構(gòu)建 基于內(nèi)存的認(rèn)證眠蚂,LDAP認(rèn)證煞聪,JDBC 認(rèn)證,或自定義認(rèn)證逝慧。
在我們的例子中昔脯,我們提供了自己的customUserDetailsServicepasswordEncoder去構(gòu)建AuthenticationManager
我們后面將會(huì)使用配置好的AuthenticationManager在登錄接口中驗(yàn)證用戶笛臣。

8.HttpSecurity configurations

HttpSecurity configurations用于配置安全功能云稚,像 csrf, sessionManagement,也可以通過多種條件配置規(guī)則來(lái)保護(hù)資源沈堡。
在我們的例子中静陈,我們給予靜態(tài)資源和一些任意用戶可訪問的API公開權(quán)限,同時(shí)也限制了一些API只能被登錄的用戶所訪問诞丽。
我們當(dāng)然也可以把JWTAuthenticationEntryPoint和自定義的JWTAuthenticationFilter配置到HttpSecurity中鲸拥。

創(chuàng)建自定義Spring Security類,F(xiàn)ilter僧免,Annotations

在上一節(jié)刑赶,我們將許多自定義的類和Filters與Spring Security結(jié)合,在這一節(jié)猬膨,我們將逐一定義這些類角撞。
以下所創(chuàng)建的所有類都在包com.example.poll.security中呛伴。

1.自定義Spring Security AuthenticationEntryPoint

我們第一個(gè)定義的與Spring Security相關(guān)的類是 JwtAuthenticationEntryPoint勃痴。他實(shí)現(xiàn)了AuthenticationEntryPoint接口并實(shí)現(xiàn)了接口中的commence()方法。當(dāng)一個(gè)未經(jīng)認(rèn)證過的用戶嘗試訪問一個(gè)需要認(rèn)證才可以訪問的資源時(shí)热康,這個(gè)方法將被調(diào)用沛申。
在本例中,我們簡(jiǎn)化response僅包含401錯(cuò)誤碼和一些異常信息姐军。

package com.example.polls.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
    @Override
    public void commence(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse,
                         AuthenticationException e) throws IOException, ServletException {
        logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
    }
}

2. 自定義Spring Security UserDetails

下一步铁材,讓我們自定義UserPrincipal類實(shí)現(xiàn)UserDetails類。此類是UserDetails類的實(shí)現(xiàn)奕锌,在我們自定義的UserDetailService作為結(jié)果返回著觉。Spring Security會(huì)使用存儲(chǔ)在UserPrincipal對(duì)象的數(shù)據(jù)來(lái)進(jìn)行認(rèn)證和授權(quán)。
以下就是完整的UserPrincipal類代碼 -

package com.example.polls.security;

import com.example.polls.model.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class UserPrincipal implements UserDetails {
    private Long id;

    private String name;

    private String username;

    @JsonIgnore
    private String email;

    @JsonIgnore
    private String password;

    private Collection<? extends GrantedAuthority> authorities;

    public UserPrincipal(Long id, String name, String username, String email, String password, Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.name = name;
        this.username = username;
        this.email = email;
        this.password = password;
        this.authorities = authorities;
    }

    public static UserPrincipal create(User user) {
        List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
                new SimpleGrantedAuthority(role.getName().name())
        ).collect(Collectors.toList());

        return new UserPrincipal(
                user.getId(),
                user.getName(),
                user.getUsername(),
                user.getEmail(),
                user.getPassword(),
                authorities
        );
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserPrincipal that = (UserPrincipal) o;
        return Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {

        return Objects.hash(id);
    }
}

3. 自定義Spring Security UserDetailService

現(xiàn)在我們來(lái)完成自定義的UserDetailService惊暴,可以通過username查找到User信息饼丘。

package com.example.polls.security;

import com.example.polls.model.User;
import com.example.polls.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.transaction.annotation.Transactional;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String usernameOrEmail)
            throws UsernameNotFoundException {
        // Let people login with either username or email
        User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
                .orElseThrow(() -> 
                        new UsernameNotFoundException("User not found with username or email : " + usernameOrEmail)
        );

        return UserPrincipal.create(user);
    }

    // This method is used by JWTAuthenticationFilter
    @Transactional
    public UserDetails loadUserById(Long id) {
        User user = userRepository.findById(id).orElseThrow(
            () -> new UsernameNotFoundException("User not found with id : " + id)
        );

        return UserPrincipal.create(user);
    }
}

第一個(gè)方法loadUserByUsername()是為Spring Security提供的,注意findByUsernameOrEmail方法辽话,他是可以用usernameemail登錄的肄鸽。
第二個(gè)方法loadUserById()是為JWTAuthenticationFilter提供的卫病,我們稍后定義它。

4. 生成和驗(yàn)證Token的實(shí)用類

下面這個(gè)類的作用在用戶登錄成功后生成JWT典徘,驗(yàn)證請(qǐng)求中頭部的JWT授權(quán)信息蟀苛。

package com.example.polls.security;

import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
public class JwtTokenProvider {

    private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

    @Value("${app.jwtSecret}")
    private String jwtSecret;

    @Value("${app.jwtExpirationInMs}")
    private int jwtExpirationInMs;

    public String generateToken(Authentication authentication) {

        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);

        return Jwts.builder()
                .setSubject(Long.toString(userPrincipal.getId()))
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    public Long getUserIdFromJWT(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(token)
                .getBody();

        return Long.parseLong(claims.getSubject());
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException ex) {
            logger.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            logger.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            logger.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            logger.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            logger.error("JWT claims string is empty.");
        }
        return false;
    }
}

上面這個(gè)類用到了@Valueproperties中讀取JWT secretexpiration time
讓我們?cè)谂渲梦募屑由线@2個(gè)屬性的配置吧 -

JWT Properties

## App Properties
app.jwtSecret= JWTSuperSecretKey
app.jwtExpirationInMs = 604800000

5. 定義Spring Security AuthenticationFilter

最后逮诲,我們創(chuàng)建JWTAuthenticationFilter來(lái)從request中拿到JWT token并驗(yàn)證它帜平,建立token與用戶之間的聯(lián)系,并在Spring Security中放行梅鹦。

package com.example.polls.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);

            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                Long userId = tokenProvider.getUserIdFromJWT(jwt);

                UserDetails userDetails = customUserDetailsService.loadUserById(userId);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }

        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null;
    }
}

在上面這個(gè)過濾器里罕模,我們首先從請(qǐng)求頭的Authorization中拿到JWT解析,拿到了用戶ID帘瞭。其后淑掌,我們從數(shù)據(jù)庫(kù)中拿到了用戶具體信息并將驗(yàn)證信息放到了Spring Security的上下文中。

注意蝶念,在filter中通過查詢數(shù)據(jù)庫(kù)拿到用戶信息是可選的抛腕。你也可以將用戶的賬號(hào),密碼及角色信息編碼后放到JWT claims中媒殉,然后通過解析JWT的claims去創(chuàng)建一個(gè)UserDetails担敌。如此便不會(huì)產(chǎn)生數(shù)據(jù)庫(kù)IO。

然而廷蓉,從數(shù)據(jù)庫(kù)中讀取用戶信息還是非常有用的全封。比如,當(dāng)用戶角色更改后或用戶在創(chuàng)建JWT后更改了密碼桃犬,則不應(yīng)讓他還用之前的JWT登錄刹悴。

6. 獲取當(dāng)前登錄用戶的自定義注解

Spring Security提供了一個(gè)叫@AuthenticationPrincipal的注解去在Controller中獲取當(dāng)前登錄的且被認(rèn)證通過的用戶。
以下 的 @CurrentUser注解頭部添加了@AuthenticationPrincipal注解攒暇。

package com.example.polls.security;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import java.lang.annotation.*;

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {

}

我們創(chuàng)建了一個(gè)元注解(注解的注解)的目的是為了在我們項(xiàng)目中不用到處與Spring Security的相關(guān)注解打交道土匀。減少了對(duì)Spring Security的依賴。如果我們決定不再使用Spring Security了形用,我們也可以輕松的更改@CurrentUser來(lái)做到就轧。

編寫登錄和注冊(cè)API

伙計(jì)們,我們已經(jīng)把我們需要的安全配置都搞定了田度,是時(shí)候來(lái)編寫登錄和注冊(cè)的API了妒御。

但在定義這些API之前,我們需要先定義API需要使用到的 請(qǐng)求體和返回體镇饺。

所有的這些請(qǐng)求體和返回體我們定義在包com.example.polls.payload中乎莉。

Request Payloads

1. LoginRquest

package com.example.polls.payload;

import javax.validation.constraints.NotBlank;

public class LoginRequest {
    @NotBlank
    private String usernameOrEmail;

    @NotBlank
    private String password;

    public String getUsernameOrEmail() {
        return usernameOrEmail;
    }

    public void setUsernameOrEmail(String usernameOrEmail) {
        this.usernameOrEmail = usernameOrEmail;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

2. SignUpRequest

package com.example.polls.payload;

import javax.validation.constraints.*;

public class SignUpRequest {
    @NotBlank
    @Size(min = 4, max = 40)
    private String name;

    @NotBlank
    @Size(min = 3, max = 15)
    private String username;

    @NotBlank
    @Size(max = 40)
    @Email
    private String email;

    @NotBlank
    @Size(min = 6, max = 20)
    private String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

Response Payloads

1. JwtAuthenticationResponse

package com.example.polls.payload;

public class JwtAuthenticationResponse {
    private String accessToken;
    private String tokenType = "Bearer";

    public JwtAuthenticationResponse(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getTokenType() {
        return tokenType;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }
}

2. ApiResponse

package com.example.polls.payload;

public class ApiResponse {
    private Boolean success;
    private String message;

    public ApiResponse(Boolean success, String message) {
        this.success = success;
        this.message = message;
    }

    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

自定義業(yè)務(wù)異常

請(qǐng)求非法或一些超預(yù)期情況的發(fā)生時(shí),API會(huì)拋出異常。

我們需要在返回體中展現(xiàn)出不同類型的異常應(yīng)有對(duì)應(yīng)的 HTTP code梦鉴。

讓我們用@ResponseStatus注解開始定義異常吧(所有異常相關(guān)代碼都定義在com.example.polls.exception

1. AppException

package com.example.polls.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class AppException extends RuntimeException {
    public AppException(String message) {
        super(message);
    }

    public AppException(String message, Throwable cause) {
        super(message, cause);
    }
}

2. BadRequestException

package com.example.polls.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {

public BadRequestException(String message) {
    super(message);
}

public BadRequestException(String message, Throwable cause) {
    super(message, cause);
}

}

3. ResourceNotFoundException

package com.example.polls.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    private String resourceName;
    private String fieldName;
    private Object fieldValue;

    public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) {
        super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
        this.resourceName = resourceName;
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }

    public String getResourceName() {
        return resourceName;
    }

    public String getFieldName() {
        return fieldName;
    }

    public Object getFieldValue() {
        return fieldValue;
    }
}

Authentication Controller

最后李茫,AuthController包含了登錄和注冊(cè)的接口。(所有Controller應(yīng)放在包com.example.polls.controller下)-

package com.example.polls.controller;

import com.example.polls.exception.AppException;
import com.example.polls.model.Role;
import com.example.polls.model.RoleName;
import com.example.polls.model.User;
import com.example.polls.payload.ApiResponse;
import com.example.polls.payload.JwtAuthenticationResponse;
import com.example.polls.payload.LoginRequest;
import com.example.polls.payload.SignUpRequest;
import com.example.polls.repository.RoleRepository;
import com.example.polls.repository.UserRepository;
import com.example.polls.security.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import javax.validation.Valid;
import java.net.URI;
import java.util.Collections;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    UserRepository userRepository;

    @Autowired
    RoleRepository roleRepository;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    JwtTokenProvider tokenProvider;

    @PostMapping("/signin")
    public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {

        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsernameOrEmail(),
                        loginRequest.getPassword()
                )
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String jwt = tokenProvider.generateToken(authentication);
        return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
    }

    @PostMapping("/signup")
    public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
        if(userRepository.existsByUsername(signUpRequest.getUsername())) {
            return new ResponseEntity(new ApiResponse(false, "Username is already taken!"),
                    HttpStatus.BAD_REQUEST);
        }

        if(userRepository.existsByEmail(signUpRequest.getEmail())) {
            return new ResponseEntity(new ApiResponse(false, "Email Address already in use!"),
                    HttpStatus.BAD_REQUEST);
        }

        // Creating user's account
        User user = new User(signUpRequest.getName(), signUpRequest.getUsername(),
                signUpRequest.getEmail(), signUpRequest.getPassword());

        user.setPassword(passwordEncoder.encode(user.getPassword()));

        Role userRole = roleRepository.findByName(RoleName.ROLE_USER)
                .orElseThrow(() -> new AppException("User Role not set."));

        user.setRoles(Collections.singleton(userRole));

        User result = userRepository.save(user);

        URI location = ServletUriComponentsBuilder
                .fromCurrentContextPath().path("/api/users/{username}")
                .buildAndExpand(result.getUsername()).toUri();

        return ResponseEntity.created(location).body(new ApiResponse(true, "User registered successfully"));
    }
}

開啟跨域

react需要從自己的端訪問到服務(wù)端的這些API肥橙。為了允許跨域訪問這些接口魄宏,我們需要?jiǎng)?chuàng)建WebMvcConfig類在包com.example.polls.config下。

package com.example.polls.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final long MAX_AGE_SECS = 3600;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE")
                .maxAge(MAX_AGE_SECS);
    }
}

檢測(cè)當(dāng)前配置并啟動(dòng)程序

如果你根據(jù)以上一步步完成了下來(lái)存筏,你的項(xiàng)目結(jié)構(gòu)應(yīng)該如下-


spring-boot-spring-security-jwt-customuserdetails-jwtauthentication-filter-directory-structure-part-2.jpg

你可以在當(dāng)前項(xiàng)目根目錄下宠互,用終端輸入一下命令來(lái)啟動(dòng)項(xiàng)目:

mvn spring-boot:run

測(cè)試登錄和注冊(cè)API

注冊(cè)

Spring-Security-JWT-User-Registration.jpg

登錄

spring-security-jwt-user-login.jpg

調(diào)用受保護(hù)API

一旦你從登錄接口獲得了返回的token,你就可以通過把token放到請(qǐng)求頭的Authorization中去調(diào)用受保護(hù)的API椭坚,就像下面這樣-

Authorization: Bearer <accessToken>

JwtAuthenticationFilter將會(huì)從請(qǐng)求頭讀取token予跌,驗(yàn)證它而判斷是否有權(quán)限去訪問這些API。

下一步是什么善茎?

哦吼券册!我們?cè)诒疚恼掠懻摿撕芏唷2⑹褂肧pring Security和JWT構(gòu)建了可靠的身份認(rèn)證和授權(quán)邏輯垂涯。感謝一直閱讀到最后烁焙。
在下一章中,我們會(huì)編寫創(chuàng)建調(diào)查和投票耕赘,獲取用戶等API骄蝇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市操骡,隨后出現(xiàn)的幾起案子九火,更是在濱河造成了極大的恐慌,老刑警劉巖册招,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岔激,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡跨细,警方通過查閱死者的電腦和手機(jī)鹦倚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)冀惭,“玉大人,你說我怎么就攤上這事掀鹅∩⑿荩” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵乐尊,是天一觀的道長(zhǎng)戚丸。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么限府? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任夺颤,我火速辦了婚禮,結(jié)果婚禮上胁勺,老公的妹妹穿的比我還像新娘世澜。我一直安慰自己,他們只是感情好署穗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布寥裂。 她就那樣靜靜地躺著,像睡著了一般案疲。 火紅的嫁衣襯著肌膚如雪封恰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天褐啡,我揣著相機(jī)與錄音诺舔,去河邊找鬼。 笑死备畦,一個(gè)胖子當(dāng)著我的面吹牛混萝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播萍恕,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼逸嘀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了允粤?” 一聲冷哼從身側(cè)響起崭倘,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎类垫,沒想到半個(gè)月后司光,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡悉患,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年残家,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片售躁。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡坞淮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出陪捷,到底是詐尸還是另有隱情回窘,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布市袖,位于F島的核電站啡直,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜酒觅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一撮执、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧舷丹,春花似錦抒钱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至装获,卻和暖如春瑞信,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背穴豫。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工凡简, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人精肃。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓秤涩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親司抱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子筐眷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容