Spring Security 整合 JSON Web Token(JWT) 提升 REST 安全性

一、背景

由于本人最近在維護 SSM 的項目,因為上一個人使用 Spring Security 這個安全框架,所以在這里也不改了,繼續(xù)使用這個框架(勇敢地往坑里跳吧),而我們的這個項目主要是為移動 APP 搭建的后臺。我們都知道 APP 對 cookie 的操作都是力不用心的,不同于 WEB 開發(fā),瀏覽器會主動來維護這個狀態(tài),這個 cookie 的名稱為 JSESSIONID,而 HTTP 是無狀態(tài)的,但也可以通過 http://api.example.com/user;jsessionid=xxxx?p=a 這樣的方式傳遞給服務(wù)器,服務(wù)器就能識別出對應的 session。現(xiàn)在的狀況是 APP 的每個請求都用這個方式傳遞,不然服務(wù)器就認為該請求未登錄,所以會被安全框架攔截,從而拿不到正常的數(shù)據(jù)。而服務(wù)器每次都要維護這么多的 session,會占用內(nèi)存,以后做集群也不方便(用 Spring Session 緩存到 Rides 中也是可以做集群的,基于 WEB 應用),那有沒有更好的解決方案呢?答案當然是有的啦!(這不是廢話,哈哈)我們可以基于 token,也可以用 OAuth2(但對于我們這些只給自己應用而不是提供給第三方使用的,用這個就有點雍腫了),然后最近使用某歌、某度,發(fā)現(xiàn)有個簡單安全的一個標準,deng deng deng deng,那就是 JSON Web Token 簡稱 JWT,所以決定用它來代替這個被我們的安卓小伙吐槽得不能再吐槽的基于 cookie 的認證方式了,Here we go!

二、JWT

什么是 JWT JSON Web Token 呢?網(wǎng)上的大神們已經(jīng)描述得非常清晰了,畢竟人家是專業(yè)人士,我只是一只在不斷學習的菜鳥,所以這里就不說了。這里提供一些我通過 xx 度搜到技術(shù)文章,里面有更通俗易懂的解說。

推薦閱讀:

三、Spring Security 與 JWT

使用 Spring Security 默認的登錄驗證是從 cookie 和 session 中過濾一個名為 JSESSIONID 的字段,因為在登錄成功后會把驗證信息保存到這個對應的 session 中,所以根據(jù) JSESSIONID 的值取出對應的 session 就能獲取是否已認證,和相應的用戶權(quán)限了。

而這里想要集成 JWT,這里就使用自己提供的 Filter,由我們來自定義一個基于 JWT 的 Filter。

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final Log logger = LogFactory.getLog(this.getClass());

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

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

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String authToken = request.getHeader(this.tokenHeader);
        // authToken.startsWith("Bearer ")
        // String authToken = header.substring(7);
        String username = jwtTokenUtil.getUsernameFromToken(authToken);

        logger.info("checking authentication für user " + username);

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

            // It is not compelling necessary to load the use details from the database. You could also store the information
            // in the token and read it from it. It's up to you ;)
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            // For simple validation it is completely sufficient to just check the token integrity. You don't have to call
            // the database compellingly. Again it's up to you ;)
            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);
    }
}

這個代碼是國外的大神寫的

當然這個代碼只能做參考,因為我們要基于自己的業(yè)務(wù)邏輯進行修改的,客官先別急,下面再來實現(xiàn)我們自己的 Filter,這里先做一下鋪墊。

** AuthenticationEntryPoint **

這個是攔截成功,需要登錄權(quán)限時會走這個,上面的大神中有使用這個

<http pattern="/api/**" entry-point-ref="restAuthenticationEntryPoint" create-session="stateless"> (3)
      <csrf disabled="true"/>  (4)
      <custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter"/>  (5)
</http>

然后他自定義了一個實現(xiàn)類

RestAuthenticationEntryPoint.java

public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // This is invoked when user tries to access a secured REST resource without supplying any credentials
        // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

他這里只是在認證失敗時返回 401 狀態(tài)碼,其實在這里我們也可以重定向到某個頁面,這個就是相當于我們在 spring-security.xml 中配置的 form-login 標簽的 login-page 屬性,認證失敗會把這個跳轉(zhuǎn)動作交給入口點來處理,詳情,現(xiàn)在我覺得就使用認的自動跳轉(zhuǎn)到我們配置文件中 login-page 就可以了,所以這個就不需要引用了。

這里依然使用可緩存的 UserDetails,緩存 UserDetails

緩存 UserDetails

參閱:

四、自定義實現(xiàn)

好了,開始擼吧!

自定義 UserDetails

由于這里整合 JWT 時利用 token 傳輸一些非敏感的關(guān)鍵信息

{
   "exp": 1491547421,
   "enabled": true,
   "sub": "13800138000",
   "scope": ["ROLE_USER"],
   "non_locked": true,
   "non_expired": true,
   "jti": "41dca17b-ce77-4308-8cc3-f9a56bffe81c",
   "user_id": 66666,
   "iat": 1491541421
}

因為這些信息在驗證時會還原成一個框架要求的完整用戶,框架自帶的子類無法滿足,所以我們自定一個實現(xiàn)類來保存這些信息

/**
 * JWT保存的用戶信息
 *
 * @author ybin
 * @since 2017-04-05
 */
public class JWTUserDetails implements UserDetails {

    private Long userId;
    private String password;
    private final String username;
    private final Collection<? extends GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;


    public JWTUserDetails(long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(userId, username, password, true, true, true, true, authorities);
    }

    public JWTUserDetails(long userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        if (username != null && !"".equals(username) && password != null) {
            this.userId = userId;
            this.username = username;
            this.password = password;
            this.enabled = enabled;
            this.accountNonExpired = accountNonExpired;
            this.credentialsNonExpired = credentialsNonExpired;
            this.accountNonLocked = accountNonLocked;
            this.authorities = authorities;
        } else {
            throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
        }
    }

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

    public long getUserId() {
        return userId;
    }

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

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

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

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

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

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

修改相應的 UserDetailsService 返回我們自定義的 UserDetails JWTUserDetails

/**
 * 提供認證所需的用戶信息
 *
 * @author ybin
 * @since 2017-03-08
 */
public class UserDetailsServiceCustom implements UserDetailsService {

    protected final Log logger = LogFactory.getLog(this.getClass());

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 1. 根據(jù)用戶標識獲取用戶
        ...

        if (user == null) {
            logger.debug("can not find user: " + username);
            throw new UsernameNotFoundException("can not find user.");
        }

        // 2. 獲取用戶權(quán)限
        ...

        UserDetails userDetails = new JWTUserDetails(userId, username, password,
                enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);

        return userDetails;
    }

現(xiàn)在有了自定義的 UserDetails,那我們怎么從 token 中還原成一個實體呢?那么就要提供一個自己 Filter 了

自定義 Filter

/**
 * JWT認證令牌過濾器
 *
 * @author ybin
 * @since 2017-04-05
 */
public class JWTAuthenticationFilter extends OncePerRequestFilter {

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

    @Resource
    private JWTUtils jwtUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        String auth_token = request.getHeader(this.token_header);
        final String auth_token_start = "Bearer ";
        if (StringUtils.isNotEmpty(auth_token) && auth_token.startsWith(auth_token_start)) {
            auth_token = auth_token.substring(auth_token_start.length());
        } else {
            // 不按規(guī)范,不允許通過驗證
            auth_token = null;
        }

        String username = jwtUtils.getUsernameFromToken(auth_token);
        logger.info(String.format("Checking authentication for user %s.", username));

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // It is not compelling necessary to load the use details from the database. You could also store the information
            // in the token and read it from it. It's up to you ;)
            // UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            UserDetails userDetails = jwtUtils.getUserFromToken(auth_token);

            // For simple validation it is completely sufficient to just check the token integrity. You don't have to call
            // the database compellingly. Again it's up to you ;)
            if (jwtUtils.validateToken(auth_token, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                logger.info(String.format("Authenticated user %s, setting security context", username));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        chain.doFilter(request, response);
    }

}

這樣就可以還原這個已登錄的用戶了,就不用每次去利用 this.userDetailsService.loadUserByUsername(username); 調(diào)用數(shù)據(jù)庫了。

Filter 也有了,接下來的怎么用呢?配置 spring-security.xml

<http use-expressions="false" create-session="stateless">
    <!-- 關(guān)閉 CSRF 保護 -->
    <csrf disabled="true"/>

    <custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter"/>

    <!-- #################### 不需要控制權(quán)限 start #################### -->
    <!-- 登錄頁面 -->
    <intercept-url pattern="/account/login" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
    <!--<!– session 超時頁面 –>-->
    <!--<intercept-url pattern="/account/invalid/session" access="IS_AUTHENTICATED_ANONYMOUSLY"/>-->
    <!-- #################### 不需要控制權(quán)限  end  #################### -->

    <!-- #################### 需要控制權(quán)限  start  #################### -->
    <!-- 訪問其他所有頁面都需要有USER權(quán)限 -->
    <intercept-url pattern="/**" access="ROLE_USER" />
    <!-- #################### 需要控制權(quán)限   end   #################### -->

    <!-- 配置登錄頁面地址login-page、登錄失敗后的跳轉(zhuǎn)地址authentication-failure-url -->
    <form-login login-page="/account/login" />
    <!--<!– 已經(jīng)超時的 sessionId 進行請求需要重定向的頁面 –>-->
    <!--<session-management invalid-session-url="/account/invalid/session">-->
        <!--<!– 設(shè)置一個帳號同時允許登錄多少次 –>-->
        <!--<concurrency-control max-sessions="1" />-->
    <!--</session-management>-->
</http>

<beans:bean id="jwtUtils" class="org.spring.security.jwt.JWTUtils"/>
<beans:bean id="jwtAuthenticationFilter" class="org.spring.security.filter.JWTAuthenticationFilter"/>

因為我們這里基于 JWT 了,所以不再需要以前那惡心的 session 了,所以設(shè)為 create-session="stateless",對應之前的 session 管理的相關(guān)標簽也要注釋掉,否則會在運行項目時發(fā)生如下沖突

07-Apr-2017 13:41:19.869 嚴重 [RMI TCP Connection(3)-127.0.0.1] org.apache.catalina.core.StandardContext.listenerStart Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener
 org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: session-management  cannot be used in combination with create-session='STATELESS' Offending resource: file .../spring-security.xml

JWTUtils.java

/**
 * jwt-token工具類
 *
 * @author ybin
 * @since 2017-04-04
 */
public class JWTUtils {

    public static final String ROLE_REFRESH_TOKEN = "ROLE_REFRESH_TOKEN";

    private static final String CLAIM_KEY_USER_ID = "user_id";
    private static final String CLAIM_KEY_AUTHORITIES = "scope";
    private static final String CLAIM_KEY_ACCOUNT_ENABLED = "enabled";
    private static final String CLAIM_KEY_ACCOUNT_NON_LOCKED = "non_locked";
    private static final String CLAIM_KEY_ACCOUNT_NON_EXPIRED = "non_expired";

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

    @Value("${jwt.access_token.expiration}")
    private Long access_token_expiration;

    @Value("${jwt.refresh_token.expiration}")
    private Long refresh_token_expiration;

    private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;

    public JWTUserDetails getUserFromToken(String token) {
        JWTUserDetails user;
        try {
            final Claims claims = getClaimsFromToken(token);
            long userId = getUserIdFromToken(token);
            String username = claims.getSubject();
            List roles = (List) claims.get(CLAIM_KEY_AUTHORITIES);
            Collection<? extends GrantedAuthority> authorities = parseArrayToAuthorities(roles);
            boolean account_enabled = (Boolean) claims.get(CLAIM_KEY_ACCOUNT_ENABLED);
            boolean account_non_locked = (Boolean) claims.get(CLAIM_KEY_ACCOUNT_NON_LOCKED);
            boolean account_non_expired = (Boolean) claims.get(CLAIM_KEY_ACCOUNT_NON_EXPIRED);

            user = new JWTUserDetails(userId, username, "password", account_enabled, account_non_expired, true, account_non_locked, authorities);
        } catch (Exception e) {
            user = null;
        }
        return user;
    }

    public long getUserIdFromToken(String token) {
        long userId;
        try {
            final Claims claims = getClaimsFromToken(token);
            userId = (Long) claims.get(CLAIM_KEY_USER_ID);
        } catch (Exception e) {
            userId = 0;
        }
        return userId;
    }

    public String getUsernameFromToken(String token) {
        String username;
        try {
            final Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = getClaimsFromToken(token);
            created = claims.getIssuedAt();
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

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

    private Date generateExpirationDate(long expiration) {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    public String generateAccessToken(UserDetails userDetails) {
        JWTUserDetails user = (JWTUserDetails) userDetails;
        Map<String, Object> claims = generateClaims(user);
        claims.put(CLAIM_KEY_AUTHORITIES, JSON.toJSON(authoritiesToArray(user.getAuthorities())));
        return generateAccessToken(user.getUsername(), claims);
    }

    private Map<String, Object> generateClaims(JWTUserDetails user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USER_ID, user.getUserId());
        claims.put(CLAIM_KEY_ACCOUNT_ENABLED, user.isEnabled());
        claims.put(CLAIM_KEY_ACCOUNT_NON_LOCKED, user.isAccountNonLocked());
        claims.put(CLAIM_KEY_ACCOUNT_NON_EXPIRED, user.isAccountNonExpired());
        return claims;
    }

    private String generateAccessToken(String subject, Map<String, Object> claims) {
        return generateToken(subject, claims, access_token_expiration);
    }

    private List authoritiesToArray(Collection<? extends GrantedAuthority> authorities) {
        List<String> list = new ArrayList<>();
        for (GrantedAuthority ga : authorities) {
            list.add(ga.getAuthority());
        }
        return list;
    }

    private Collection<? extends GrantedAuthority> parseArrayToAuthorities(List roles) {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        SimpleGrantedAuthority authority;
        for (Object role : roles) {
            authority = new SimpleGrantedAuthority(role.toString());
            authorities.add(authority);
        }
        return authorities;
    }

    public String generateRefreshToken(UserDetails userDetails) {
        JWTUserDetails user = (JWTUserDetails) userDetails;
        Map<String, Object> claims = generateClaims(user);
        // 只授于更新 token 的權(quán)限
        String roles[] = new String[]{JWTUtils.ROLE_REFRESH_TOKEN};
        claims.put(CLAIM_KEY_AUTHORITIES, JSON.toJSON(roles));
        return generateRefreshToken(user.getUsername(), claims);
    }

    private String generateRefreshToken(String subject, Map<String, Object> claims) {
        return generateToken(subject, claims, refresh_token_expiration);
    }

    public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
        final Date created = getCreatedDateFromToken(token);
        return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
                && (!isTokenExpired(token));
    }

    public String refreshToken(String token) {
        String refreshedToken;
        try {
            final Claims claims = getClaimsFromToken(token);
            refreshedToken = generateAccessToken(claims.getSubject(), claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    private String generateToken(String subject, Map<String, Object> claims, long expiration) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(new Date())
                .setExpiration(generateExpirationDate(expiration))
                .compressWith(CompressionCodecs.DEFLATE)
                .signWith(SIGNATURE_ALGORITHM, secret)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        JWTUserDetails user = (JWTUserDetails) userDetails;
        final long userId = getUserIdFromToken(token);
        final String username = getUsernameFromToken(token);
        // final Date created = getCreatedDateFromToken(token);
        // final Date expiration = getExpirationDateFromToken(token);
        return (userId == user.getUserId()
                && username.equals(user.getUsername())
                && !isTokenExpired(token)
                /* && !isCreatedBeforeLastPasswordReset(created, userDetails.getLastPasswordResetDate()) */
        );
    }

}

Postman 測試

測試

利用 Postman 測試一下接口,為了后續(xù)使用其他接口,這里不像 cookie 在 Postman 設(shè)置中默認會自動帶上,而我們基于 JWT 后,需要每次設(shè)置到 header 中,這樣每次手動設(shè)置(想砸電腦的心都有了),這里推薦個小技巧,使用 Postman 自帶的功能實現(xiàn)請求成功后自動幫我們設(shè)置一個全局的變量,然后在其他接口就能引用了,再也不用一個個 Ctrl + CCtrl + V 了,是不是很棒棒呢

skill

這樣我們就完成了使用 JWT 的方式來代替原有的 JSESSIONID 基于 session 和 cookie 的方式了,仿佛看到新大陸。

refresh token

細心的人可能已經(jīng)發(fā)現(xiàn)上圖中的 API 請求返回了一個 refresh_token,這玩意有啥子用??可以參考下 微信的開放文檔 里面有提及到這個思想。

刷新access_token有效期


access_token是調(diào)用授權(quán)關(guān)系接口的調(diào)用憑證,由于access_token有效期(目前為2個小時)較短,當access_token超時后,可以使用refresh_token進行刷新,access_token刷新結(jié)果有兩種:

  1. 若access_token已超時,那么進行refresh_token會獲取一個新的access_token,新的超時時間;
  2. 若access_token未超時,那么進行refresh_token不會改變access_token,但超時時間會刷新,相當于續(xù)期access_token。

refresh_token擁有較長的有效期(30天),當refresh_token失效的后,需要用戶重新授權(quán)。

請求方法

獲取第一步的code后,請求以下鏈接進行refresh_token:
https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN

參數(shù)說明

參數(shù) 是否必須 說明
appid 應用唯一標識
grant_type 填refresh_token
refresh_token 填寫通過access_token獲取到的refresh_token參數(shù)

返回說明

正確的返回:

{
    "access_token":"ACCESS_TOKEN",
    "expires_in":7200,
    "refresh_token":"REFRESH_TOKEN",
    "openid":"OPENID",
    "scope":"SCOPE"
}
參數(shù) 說明
access_token 接口調(diào)用憑證
expires_in access_token接口調(diào)用憑證超時時間,單位(秒)
refresh_token 用戶刷新access_token
openid 授權(quán)用戶唯一標識
scope 用戶授權(quán)的作用域,使用逗號(,)分隔

錯誤返回樣例:

{"errcode":40030,"errmsg":"invalid refresh_token"}

檢驗授權(quán)憑證(access_token)是否有效


請求說明

http請求方式: GET https://api.weixin.qq.com/sns/auth?access_token=ACCESS_TOKEN&openid=OPENID

參數(shù)說明 參數(shù) 是否必須
access_token 調(diào)用接口憑證
openid 普通用戶標識,對該公眾帳號唯一 返回說明

說明

正確的 Json 返回結(jié)果: { "errcode":0, "errmsg":"ok" }
錯誤的 Json 返回示例: { "errcode":40003, "errmsg":"invalid openid" }

就是在 access_token 過期或還沒過期的情況下刷新 access_token,而不用使用帳號密碼進行登錄獲取了。

刷新 access_token 的權(quán)限

既然提供了這個刷新的接口,那我是不是可以使用 access_token 進行刷新呢?又能不能使用 refresh_token 來代替 access_token 呢?所以問題就來,我們要在業(yè)務(wù)中加入只有 使用 refresh_token 進行刷新。細心的你可能已經(jīng)發(fā)現(xiàn)了這兩個 token 的 Payload(內(nèi)容) 中有scope 這個字段,對,這個就是當前用戶的所擁有的權(quán)限

access_token refresh_token
"scope": ["ROLE_USER"] "scope": ["ROLE_REFRESH_TOKEN"]

我們生成 refresh_token 時,只賦予 ROLE_REFRESH_TOKEN 權(quán)限,那就各施其職了。

現(xiàn)在權(quán)限也相應賦予了,但我們使用 refresh_token 請求時都是返回 403

403

這是因為我們要配置文件配置了所有接口都要有 ROLE_USER 這個權(quán)限才能訪問,所以識別不了 ROLE_REFRESH_TOKEN 這個權(quán)限,這時我們在配置文件上加上

<intercept-url pattern="/account/auth/refresh_token" access="ROLE_REFRESH_TOKEN" />
<intercept-url pattern="/**" access="ROLE_USER" />

這樣即可正常使用 refresh_token 進行訪問了。而我們使用 access_token 訪問時就出現(xiàn) 403,因為它只有 ROLE_USER 權(quán)限,但是我們現(xiàn)在是面向移動 APP 的后臺服務(wù),這樣不友好,那咋辦呢?

我目前是這么干的,給 /account/auth/refresh_token 這個路徑設(shè)置兩個訪問權(quán)限

<intercept-url pattern="/account/auth/refresh_token" access="ROLE_USER,ROLE_REFRESH_TOKEN" />

然后使用方法級別的權(quán)限校驗,如果沒有這個權(quán)限框架就會拋出 AccessDeniedException,然后我們在統(tǒng)一異常處理處識別與返回 json 格式的數(shù)據(jù)了

@GetMapping("/account/auth/refresh_token")
@PreAuthorize("hasRole('" + JWTUtils.ROLE_REFRESH_TOKEN + "')")
public String refreshAuthToken(...) {
    ...
}

注意

由于我們使用 spring security 的注解,而且是在控制層 controller 中,所以我們還要打開 spring security 對注解的支持(目前有三種不同的注解,要分別打開),這里涉及到一個細節(jié)的問題,要在 controller 層與只在 service 層使用配置的方式不一樣的,前者要配置到 spring-mvc.xml 中,后者在 spring-security.xml 中配置就可以了。我們這里就要使用第一種了,配置如下

spring-security.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans ···
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="
       http://www.springframework.org/schema/security
       http://www.springframework.org/schema/security/spring-security.xsd
       ···">

    <security:global-method-security pre-post-annotations="enabled" proxy-target-class="true"/>

    ···

</beans>

這里要引入 spring-security 相應的頭文件

還搞不清Spring 與 Spring MVC 容器之間的關(guān)系?

到此,我們就完成了,只能使用 ROLE_REFRESH_TOKEN 權(quán)限才能訪問了,不擁有這個權(quán)限的就會返回權(quán)限不足。

參閱:
java - Can Spring Security use @PreAuthorize on Spring controllers methods? - Stack Overflow

但是目前這樣也有不足,就是

  1. 只能是在這個接口使用 ROLE_REFRESH_TOKEN 是正常訪問, ROLE_USER 能返回 json 提示。
  2. 當使用 ROLE_REFRESH_TOKEN 權(quán)限訪問其他接口時就是返回網(wǎng)頁版的 403,而不是拋出權(quán)限不足的異常,從而統(tǒng)一異常處理也處理不了。

提出問題
如果你知道怎么在沒有對應權(quán)限時能統(tǒng)一返回 json 數(shù)據(jù),而不是 403 網(wǎng)頁版的,歡迎在下方留言或私聊我,在這先謝謝你!

如果你堅持看到這了,感謝您的支持,哈哈。如果文章中有那些不正確的歡迎指正,謝謝!畢竟個人的認知是有限的,歡迎指正。 ??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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