Spring Security + JWT 前后端分離

Spring Security

1耕姊、基本簡介

SpringSecurity 是企業(yè)應用系統(tǒng)的權(quán)限管理框架,應用的安全性包括用戶認證(Authentication)和用戶授權(quán)(Authorization)兩個部分病附。用戶認證一般要求用戶提供用戶名和密碼奸远。系統(tǒng)通過校驗用戶名和密碼來完成認證過程蟹肘,用戶授權(quán)指的是驗證某個用戶是否有權(quán)限執(zhí)行某個操作。在一個系統(tǒng)中饮怯,不同用戶所具有的權(quán)限是不同的。spring security的主要核心功能為認證和授權(quán)嚎研,所有的架構(gòu)也是基于這兩個核心功能去實現(xiàn)的蓖墅。

2、框架原理

總所周知临扮,想要對 Web 資源進行控制论矾,最好的莫過于加 Filter;想要對方法調(diào)用進行控制杆勇,最好的辦法莫過于 AOP贪壳。所以 SpringSecurity 在我們進行用戶認證以及授權(quán)權(quán)限的時候,通過各種各樣的 Filter 來控制權(quán)限的訪問靶橱。


  • 框架的核心組件
  1. SecurityContextHolder:提供對 SecurityContext 的訪問寥袭,底層封裝了 ThreadLocal路捧,使其管理的對象(SecurityContext )存儲在當前線程上;
  2. SecurityContext,:持有 Authentication 對象和其他可能需要的信息传黄;
  3. AuthenticationManager 其中可以包含多個AuthenticationProvider杰扫;
  4. ProviderManager 對象為 AuthenticationManager 接口的實現(xiàn)類;
  5. AuthenticationProvider 主要用來進行認證操作的類 調(diào)用其中的 authenticate() 方法去進行認證操作膘掰;
  6. Authentication:Spring Security 方式的認證主體章姓;
  7. GrantedAuthority:對認證主題的應用層面的授權(quán),含當前用戶的權(quán)限信息识埋,通常使用角色表示;
  8. UserDetails:構(gòu)建Authentication對象必須的信息凡伊,可以自定義,可能需要訪問DB得到窒舟;
  9. UserDetailsService:通過username構(gòu)建UserDetails對象系忙,通過loadUserByUsername根據(jù)userName獲取UserDetail對象 (可以在這里基于自身業(yè)務進行自定義的實現(xiàn) 如通過數(shù)據(jù)庫,xml,緩存獲取等)惠豺。

3银还、認證流程說明

當點擊登錄操作時,會到第一個攔截器UsernamePasswordAuthenticationFilterdoFilter方法,我們直接看這個類:

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 必須 POST 請求
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            // 獲取用戶名洁墙,密碼
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            // 生成 Token
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            // 進行驗證
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
}

從源碼可知蛹疯,UsernamePasswordAuthenticationFilterAbstractAuthenticationProcessingFilter的子類,故其實是走AbstractAuthenticationProcessingFilterdoFilter方法:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;

        try {
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
            unsuccessfulAuthentication(request, response, failed);

            return;
        }
        catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

        successfulAuthentication(request, response, chain, authResult);
    }

doFilter中會調(diào)用UsernamePasswordAuthenticationFilterattemptAuthentication方法热监,主要是進行 username 和 password 請求值的獲取捺弦,然后再生成一個UsernamePasswordAuthenticationToken 對象,進行驗證孝扛。
不過我們可以先看看UsernamePasswordAuthenticationToken的構(gòu)造方法:

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        // 設置空權(quán)限
        super(null);
        // 設置用戶名
        this.principal = principal;
        // 設置密碼
        this.credentials = credentials;
        // 設置是否通過了校驗
        setAuthenticated(false);
    }

其實UsernamePasswordAuthenticationToken是繼承于Authentication列吼,該對象是處理登錄成功回調(diào)方法中的一個參數(shù),里面包含了用戶信息疗琉、請求信息等參數(shù)冈欢。
接下來我們看:
this.getAuthenticationManager().authenticate(authRequest);
這里有一個AuthenticationManager,但是真正調(diào)用的是ProviderManager盈简。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,InitializingBean {
  public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
            // 判斷是否有provider支持該 Authentication
            if (!provider.supports(toTest)) {
                continue;
            }

            try {
                // 真正的邏輯判斷
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw e;
            }
            ...
}
  1. 這里首先通過provider判斷是否支持當前傳入進來的Authentication凑耻,目前我們使用的是UsernamePasswordAuthenticationToken,因為除了帳號密碼登錄的方式柠贤,還會有其他的方式香浩,比如SocialAuthenticationToken
  2. 根據(jù)我們目前所使用的UsernamePasswordAuthenticationToken臼勉,provider對應的是DaoAuthenticationProvider邻吭。
public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;   
            // 1.獲取 UserDetails
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        }
        try {
            // 2.用戶信息預檢查
            preAuthenticationChecks.check(user);
            // 3.附加的信息檢查(密碼檢查)
            additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
    
        }
        // 4.最后的檢查
        postAuthenticationChecks.check(user);
        // 5.返回真正經(jīng)過認證的 Authentication 
        return createSuccessAuthentication(principalToReturn, authentication, user);
}
  1. 去調(diào)用自己實現(xiàn)的UserDetailsServiceloadUserByUsername方法,返回UserDetails
  2. UserDetails的信息進行校驗宴霸,主要是帳號是否被凍結(jié)囱晴,是否過期膏蚓,用戶是否可用等
  3. 對密碼進行檢查,這里調(diào)用了PasswordEncoder
  4. 檢查UserDetailsisCredentialsNonExpired是否可用
  5. 返回經(jīng)過認證的Authentication

這里的兩次對UserDetails的檢查畸写,主要就是通過它的四個返回boolean類型的方法驮瞧。經(jīng)過信息的校驗之后,通過UsernamePasswordAuthenticationToken的構(gòu)造方法枯芬,返回了一個經(jīng)過認證的Authentication论笔。

在通過attemptAuthentication方法之后,如果認證成功千所,會調(diào)用successfulAuthentication方法:

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                    + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);

        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }

        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

該方法中有一行比較重要的代碼SecurityContextHolder.getContext().setAuthentication(authResult);
SecurityContextHolder是對于ThreadLocal的封裝狂魔。 ThreadLocal是一個線程內(nèi)部的數(shù)據(jù)存儲類,通過它可以在指定的線程中存儲數(shù)據(jù)淫痰,數(shù)據(jù)存儲以后最楷,只有在指定線程中可以獲取到存儲的數(shù)據(jù),對于其他線程來說則無法獲取到數(shù)據(jù)黑界。
最后執(zhí)行successHandler.onAuthenticationSuccess(request, response, authResult)管嬉,該方法會走登錄成功之后的操作(一般我們會自定義登錄成功之后的操作)。

如果認證失敗朗鸠,即拋AuthenticationException異常時,就會走unsuccessfulAuthentication方法:

protected void unsuccessfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
        SecurityContextHolder.clearContext();

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication request failed: " + failed.toString(), failed);
            logger.debug("Updated SecurityContextHolder to contain null Authentication");
            logger.debug("Delegating to authentication failure handler " + failureHandler);
        }

        rememberMeServices.loginFail(request, response);

        failureHandler.onAuthenticationFailure(request, response, failed);
    }

這里會清空SecurityContextHolder的值础倍,然后執(zhí)行failureHandler.onAuthenticationFailure(request, response, failed)來處理登錄失敗后的操作(一般我們會自定義登錄失敗后的操作)烛占。

JWT

JSON Web Token (JWT) 是 JSON 格式的被加密了的字符串。在傳統(tǒng)的用戶登錄認證中沟启,都是基于session的登錄認證忆家。用戶登錄成功,服務端會保存一個session德迹,當然會給客戶端一個 sessionId芽卿,客戶端會把 sessionId 保存在cookie中,每次請求都會攜帶這個 sessionId胳搞。
cookie+session這種模式通常是保存在內(nèi)存中卸例,而且服務從單服務到多服務會面臨的session共享問題,隨著用戶量的增多肌毅,開銷就會越大筷转。而 JWT 不是這樣的,只需要服務端生成token悬而,客戶端保存這個token呜舒,每次請求攜帶這個token,服務端認證解析笨奠。

JWT 的構(gòu)成

JWT 由三部分構(gòu)成袭蝗,第一部分為頭部(header)唤殴,第二部分為載荷(playload),第三部分是簽證(signature)到腥。

header

JWT 的頭部承載兩部分信息:

  • 聲明類型朵逝,這里是 JWT
  • 聲明加密的算法,通常直接使用 HMAC SHA256

完整的頭部如下:

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

然后將頭部進行base64加密(該加密是可以對稱解密的),構(gòu)成了第一部分左电。

playload

載荷就是存放有效信息的地方廉侧,這些有效信息包含三個部分

  • 標準中注冊的聲明(Registered claims)
  • 公共的聲明(Public claims)
  • 私有的聲明(Private claims)
標準中注冊的聲明(建議但不強制使用)
  • iss: jwt 簽發(fā)者
  • sub: jwt 所面向的用戶
  • aud: 接收 jwt 的一方
  • exp: jwt 的過期時間,這個過期時間必須大于簽發(fā)時間
  • nbf: 定義在什么時間之前篓足,該 jwt 都不可用
  • iat: jwt 的簽發(fā)時間
  • jti: jwt 的唯一標識
公共的聲明

公共的聲明可以添加任何的信息段誊,一般添加用戶的相關(guān)信息或其他業(yè)務需要的必要信息。但不建議添加敏感信息栈拖,因為該部分在客戶端可解密连舍。

私有的聲明

私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息涩哟,因為base64是對稱解密的索赏,意味著該部分信息可以歸類為明文信息。
定義一個 playload:

{
"name":"Free碼農(nóng)",
"age":"28",
"org":"今日頭條"
}

然后將其進行base64加密贴彼,得到 jwt 的第二部分潜腻。

signature(簽名)

JWT 的第三部分是一個簽證信息,這個簽證信息由三部分組成器仗,base64編譯過的 header 和 playload融涣,以及一個 secret 秘鑰。簽名算法是 header 中指定的那個精钮。簽名公式為:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
簽名是用于驗證消息在傳遞過程中有沒有被更改威鹿,并且,對于使用私鑰簽名的token轨香,它還可以驗證 JWT的發(fā)送方是否為它所指定的發(fā)送方忽你。

JWT 的三個部分,是以.分隔的臂容。如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmciOiLku4rml6XlpLTmnaEiLCJuYW1lIjoiRnJlZeeggeWGnCIsImV4cCI6MTUxNDM1NjEwMywiaWF0IjoxNTE0MzU2MDQzLCJhZ2UiOiIyOCJ9.49UF72vSkj-sA4aHHiYN5eoZ9Nb4w5Vb45PsLF7x_NY

SpringSecurity + JWT 代碼實現(xiàn)

  • 導入依賴
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
  • 首先創(chuàng)建一個 JwtUser 實現(xiàn) UserDetails
    org.springframework.security.core.userdetails.UserDetails
    先看一下這個接口的源碼科雳,其實很簡單
public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

這個是Spring Security給我們提供的一個簡單的接口,因為我們需要通過SecurityContextHolder去取得用戶憑證等等信息策橘。

package com.yongda.security;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @author K. L. Mao
 * @create 2019/1/10
 */
public class JwtUser implements UserDetails {

    private String username;

    private String password;

    private Collection<? extends GrantedAuthority> authorities;

    public JwtUser(String username, String password, Integer state, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.state = state;
        this.authorities = authorities;
    }

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

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

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

    // 賬戶是否未過期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 賬戶是否未被鎖
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • 編寫一個工具類來生成令牌等操作
package com.yongda.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 生成令牌炸渡,驗證等等一些操作
 * @author K. L. Mao
 * @create 2019/1/10
 */
@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtil {

    private String secret;

    // 過期時間 毫秒
    private Long expiration;

    private String header;

    /**
     * 從數(shù)據(jù)聲明生成令牌
     *
     * @param claims 數(shù)據(jù)聲明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 從令牌中獲取數(shù)據(jù)聲明
     *
     * @param token 令牌
     * @return 數(shù)據(jù)聲明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成令牌
     *
     * @param userDetails 用戶
     * @return 令牌
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put(Claims.SUBJECT, userDetails.getUsername());
        claims.put(Claims.ISSUED_AT, new Date());
        return generateToken(claims);
    }

    /**
     * 從令牌中獲取用戶名
     *
     * @param token 令牌
     * @return 用戶名
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 判斷令牌是否過期
     *
     * @param token 令牌
     * @return 是否過期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return true;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put(Claims.ISSUED_AT, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 驗證令牌
     *
     * @param token       令牌
     * @param userDetails 用戶
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        JwtUser user = (JwtUser) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }
}

@ConfigurationProperties(prefix = "jwt")讀取配置文件以 "jwt" 前綴的配置信息。

  • 編寫一個 Filter
package com.yongda.filter;

import com.yongda.security.JwtTokenUtil;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
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;

/**
 * 攔截器
 * @author K. L. Mao
 * @create 2019/1/11
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String token = request.getHeader(jwtTokenUtil.getHeader());
        if (!StringUtils.isEmpty(token)) {
            String username = jwtTokenUtil.getUsernameFromToken(token);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null){
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(token, userDetails)){
                    // 將用戶信息存入 authentication丽已,方便后續(xù)校驗
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    // 將 authentication 存入 ThreadLocal蚌堵,方便后續(xù)獲取用戶信息
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

此過濾器主要是驗證令牌的合法性,如果令牌合法,則獲取用戶信息吼畏,并且存入SecurityContextHolder督赤。

  • JwtUserDetailsServiceImpl
    JwtUserDetailsServiceImpl這個實現(xiàn)類是實現(xiàn)了UserDetailsServiceUserDetailsService是 Spring Security 進行身份驗證的時候會使用泻蚊,我們這里就一個加載用戶信息的簡單方法loadUserByUsername躲舌,就是得到當前登錄用戶的一些用戶名、密碼性雄、用戶所擁有的角色等等一些信息没卸。
package com.yongda.security;

import com.yongda.model.Role;
import com.yongda.model.User;
import com.yongda.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.List;
import java.util.stream.Collectors;

/**
 * @author K. L. Mao
 * @create 2019/1/11
 */
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(String.format("%s.這個用戶不存在", username));
        }
        List<SimpleGrantedAuthority> authorities = user.getRoles().stream().map(Role::getRolename).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return new JwtUser(user.getUsername(), user.getPassword(), user.getState(), authorities);
    }
}
  • 自定義登錄成功之后的操作類 MyAuthenticationSuccessHandler
package com.yongda.security.handler;

import com.alibaba.fastjson.JSONObject;
import com.yongda.exception.CodeMsg;
import com.yongda.exception.Result;
import com.yongda.security.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

/**
 * 登錄成功操作
 * @author K. L. Mao
 * @create 2019/1/15
 */
@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        SecurityContextHolder.getContext().setAuthentication(authentication);
        String token = jwtTokenUtil.generateToken(userDetails);
        renderToken(httpServletResponse, token);
    }

    /**
     * 渲染返回 token 頁面,因為前端頁面接收的都是Result對象,故使用application/json返回
     *
     * @param response
     * @throws IOException
     */
    public void renderToken(HttpServletResponse response, String token) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream out = response.getOutputStream();
        String str = JSONObject.toJSONString(Result.succes(token));
        out.write(str.getBytes("UTF-8"));
        out.flush();
        out.close();
    }
}

實現(xiàn)接口AuthenticationSuccessHandler秒旋,登錄成功约计,把用戶信息存入SecurityContextHolder,并且生成token返回給前端迁筛。

  • 自定義登錄失敗操作類 MyAuthenticationFailureHandler
package com.yongda.security.handler;

import com.yongda.exception.CodeMsg;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

/**
 * 登錄失敗操作
 * @author K. L. Mao
 * @create 2019/1/15
 */
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        CodeMsg.USERNAME_OR_PWD_ERROR.renderError(httpServletResponse);
    }
}

實現(xiàn)接口AuthenticationFailureHandler煤蚌,登錄失敗,直接返回錯誤信息給前端细卧。

  • 自定義身份認證失敗處理類 EntryPointUnauthorizedHandler
package com.yongda.security.handler;

import com.yongda.exception.CodeMsg;
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;

/**
 * 身份校驗失敗處理器尉桩,如 token 錯誤
 * @author K. L. Mao
 * @create 2019/1/11
 */
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        CodeMsg.AUTH_FAILURE.renderError(response);
    }
}

實現(xiàn)接口AuthenticationEntryPointtoken失效或者錯誤贪庙,直接返回前端認證失敗信息蜘犁。

  • 自定義權(quán)限不足處理類 RestAccessDeniedHandler
package com.yongda.security.handler;

import com.yongda.exception.CodeMsg;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * 權(quán)限校驗處理器
 * @author K. L. Mao
 * @create 2019/1/11
 */
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        CodeMsg.ACCESS_DENIED.renderError(response);
    }
}

實現(xiàn)接口AccessDeniedHandler,權(quán)限不足信息返回給前端止邮。

  • WebSecurityConfig
    這個就是Spring Security 的配置類
package com.yongda.config;

import com.yongda.filter.JwtAuthenticationTokenFilter;
import com.yongda.security.handler.EntryPointUnauthorizedHandler;
import com.yongda.security.handler.MyAuthenticationFailureHandler;
import com.yongda.security.handler.MyAuthenticationSuccessHandler;
import com.yongda.security.handler.RestAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
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.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * Spring Security 配置類
 * @EnableGlobalMethodSecurity 開啟注解的權(quán)限控制沽瘦,默認是關(guān)閉的。
 * prePostEnabled:使用表達式實現(xiàn)方法級別的控制农尖,如:@PreAuthorize("hasRole('ADMIN')")
 * securedEnabled: 開啟 @Secured 注解過濾權(quán)限,如:@Secured("ROLE_ADMIN")
 * jsr250Enabled: 開啟 @RolesAllowed 注解過濾權(quán)限良哲,如:@RolesAllowed("ROLE_ADMIN")
 *
 * @author K. L. Mao
 * @create 2019/1/11
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurity extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
    @Autowired
    private RestAccessDeniedHandler restAccessDeniedHandler;
    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    /**
     * 從容器中取出 AuthenticationManagerBuilder盛卡,執(zhí)行方法里面的邏輯之后,放回容器
     * @param authenticationManagerBuilder
     * @throws Exception
     */
    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    private PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 在 UsernamePasswordAuthenticationFilter 之前添加 JwtAuthenticationTokenFilter
          */
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 角色校驗時筑凫,會自動拼接 "ROLE_"
                .antMatchers("/user/**").hasAnyRole("ADMIN","USER")
                .antMatchers("/non-auth/**").permitAll()
                .anyRequest().authenticated()   // 任何請求,登錄后可以訪問
                .and().formLogin().loginProcessingUrl("/login")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
                .and().headers().cacheControl();

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
        //讓Spring security 放行所有preflight request(cors 預檢請求)
        registry.requestMatchers(CorsUtils::isPreFlightRequest).permitAll();
        // 處理異常情況:認證失敗和權(quán)限不足
        http.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);
    }

    @Bean
    public CorsFilter corsFilter(){
        UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
        CorsConfiguration cors = new CorsConfiguration();
        cors.setAllowCredentials(true);
        cors.addAllowedOrigin("*");
        cors.addAllowedHeader("*");
        cors.addAllowedMethod("*");
        configurationSource.registerCorsConfiguration("/**", cors);
        return new CorsFilter(configurationSource);
    }
}

@EnableGlobalMethodSecurity 開啟注解的權(quán)限控制滑沧,默認是關(guān)閉的。

  • prePostEnabled:使用表達式實現(xiàn)方法級別的控制巍实,如:@PreAuthorize("hasRole('ADMIN')")
  • securedEnabled: 開啟 @Secured 注解過濾權(quán)限滓技,如:@Secured("ROLE_ADMIN")
  • jsr250Enabled: 開啟 @RolesAllowed 注解過濾權(quán)限,如:@RolesAllowed("ROLE_ADMIN")

通過AuthenticationManagerBuilder將我們自定義的JwtUserDetailsServiceImpl和加密方式BCryptPasswordEncoder進行賦值棚潦。

http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
UsernamePasswordAuthenticationFilter之前添加 JwtAuthenticationTokenFilter令漂,讓所有請求先到JwtAuthenticationTokenFilter

formLogin().loginProcessingUrl("/login")指定登錄請求路徑,該路徑會走UsernamePasswordAuthenticationFilter進行登錄操作叠必。必須是POST請求荚孵,而且是FORM表單傳參,不能JSON傳參纬朝。

successHandler(myAuthenticationSuccessHandler)登錄成功處理器收叶,failureHandler(myAuthenticationFailureHandler)登錄失敗處理器。

http.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);捕捉權(quán)限控制異常共苛,如果是身份認證異常判没,就走entryPointUnauthorizedHandler,如果是權(quán)限不足異常隅茎,則走restAccessDeniedHandler澄峰。

至此,SpringSecurity 和 JWT 的集成配置完畢;继拧LА!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末踪蹬,一起剝皮案震驚了整個濱河市胞此,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌跃捣,老刑警劉巖漱牵,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異疚漆,居然都是意外死亡酣胀,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門娶聘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闻镶,“玉大人,你說我怎么就攤上這事丸升∶” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵狡耻,是天一觀的道長墩剖。 經(jīng)常有香客問我,道長夷狰,這世上最難降的妖魔是什么岭皂? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮沼头,結(jié)果婚禮上爷绘,老公的妹妹穿的比我還像新娘书劝。我一直安慰自己,他們只是感情好揉阎,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布庄撮。 她就那樣靜靜地躺著,像睡著了一般毙籽。 火紅的嫁衣襯著肌膚如雪洞斯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天坑赡,我揣著相機與錄音烙如,去河邊找鬼。 笑死毅否,一個胖子當著我的面吹牛亚铁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播螟加,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼徘溢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捆探?” 一聲冷哼從身側(cè)響起然爆,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎黍图,沒想到半個月后曾雕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡助被,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年剖张,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揩环。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡搔弄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丰滑,到底是詐尸還是另有隱情肯污,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布吨枉,位于F島的核電站,受9級特大地震影響哄芜,放射性物質(zhì)發(fā)生泄漏貌亭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一认臊、第九天 我趴在偏房一處隱蔽的房頂上張望圃庭。 院中可真熱鬧,春花似錦、人聲如沸剧腻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽书在。三九已至灰伟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間儒旬,已是汗流浹背栏账。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留栈源,地道東北人挡爵。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像甚垦,于是被迫代替她去往敵國和親茶鹃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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