Spring Boot+Spring security+Jwt實(shí)現(xiàn)token控制權(quán)限

主要實(shí)現(xiàn)的功能是Login拿到token,再用token請(qǐng)求資源。關(guān)于登錄用戶名密碼驗(yàn)證這個(gè)在另一篇文章有提到(Spring Boot + Security實(shí)現(xiàn)簡(jiǎn)單驗(yàn)證登錄操作),這里就主要講token的生成,驗(yàn)證以及用戶具體權(quán)限的驗(yàn)證。

GitHub代碼地址

本例子功能如下圖:


1.引入Spring Security Jwt依賴。

        <!--spring security -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

2.創(chuàng)建JwtTokenProvider類提供生成以及驗(yàn)證token的方法。

生成token主要用到四個(gè)元素:

1)username token主要的標(biāo)志

2)expireTime token過(guò)期時(shí)間(xxx ms)

3)issuedDate token的創(chuàng)建時(shí)間

4)signWith token簽名,包括簽名方法和密鑰

這里的過(guò)期時(shí)間和密鑰配在properties文件里面,代碼通過(guò)@Value拿的。

jwtTokenSecret = Sayo
tokenExpiredMs = 604800000
@Component
@PropertySource("classpath:auth.properties")
public class AuthParameters {

    private String jwtTokenSecret;
    private long tokenExpiredMs;

    public String getJwtTokenSecret() {
        return jwtTokenSecret;
    }

    @Value("${jwtTokenSecret}")
    public void setJwtTokenSecret(String jwtTokenSecret) {
        this.jwtTokenSecret = jwtTokenSecret;
    }

    public long getTokenExpiredMs() {
        return tokenExpiredMs;
    }

    @Value("${tokenExpiredMs}")
    public void setTokenExpiredMs(long tokenExpiredMs) {
        this.tokenExpiredMs = tokenExpiredMs;
    }
}
驗(yàn)證token用同樣的密鑰去解開(kāi)token

倘若能解開(kāi)則表示該token是合法可用的,解析時(shí)有可能會(huì)拋出以下5個(gè)exception,可以分別catch處理log出日志,這里都統(tǒng)一處理了。

1)ExpiredJwtException token時(shí)效過(guò)期異常

2)UnsupportedJwtException 驗(yàn)證的token和期待的token格式不一樣時(shí),例如解析的是一個(gè)明文JWT而期待的是一個(gè)加密簽名JWT時(shí)就會(huì)拋出這個(gè)異常。

3)MalformedJwtException 表示這不是一個(gè)正確方法創(chuàng)建的token。

4)SignatureException token簽名驗(yàn)證失敗異常

5)IllegalArgumentException token為null或者空異常


@Component

public class JwtTokenProvider {

    Loggerlogger = LoggerFactory.getLogger(JwtTokenProvider.class);

    @Autowired

    private AuthParametersauthParameters;

    /**

     * Generate token for user login.

     *

     * @param authentication

     * @return return a token string.

     */

     public String createJwtToken(Authentication authentication) {

        //user name

        String username = ((org.springframework.security.core.userdetails.User) authentication.getPrincipal()).getUsername();

        //expire time

        Date expireTime =new Date(System.currentTimeMillis()+authParameters.getTokenExpiredMs());

        //create token

        String token = Jwts.builder()

                      .setSubject(username)

                      .setExpiration(expireTime)

                      .setIssuedAt(new Date())

                      .signWith(SignatureAlgorithm.HS512, authParameters.getJwtTokenSecret())

                      .compact();

         return token;

    }

    /**

     * validate token eligible.

     * if Jwts can parse the token string and no throw any exception, then the token is eligible.

     * @param token a jws string.

*/

    public boolean validateToken(String token) {

        String VALIDATE_FAILED ="validate failed : ";

        try {
              Jwts.parser()
                  .setSigningKey(authParameters.getJwtTokenSecret())
                  .parseClaimsJws(token);

              return true;

        }catch (Exception ex) {

              //ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, //IllegalArgumentException

              logger.error(VALIDATE_FAILED + ex.getMessage());

              return false;

        }

}

}

image

3.創(chuàng)建JwtAuthenticationFilter類在用戶獲取資源之前讓spring去filter這個(gè)token是否合法可用。

繼承OncePerRequestFilter重寫(xiě)doFilterInternal方法,前端發(fā)送請(qǐng)求時(shí),token會(huì)放在header,在每個(gè)請(qǐng)求讀取資源之前后臺(tái)對(duì)token進(jìn)行filter。

public class JwtAuthenticationFilter extends OncePerRequestFilter {

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

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Autowired
    private AuthParameters authParameters;

    @Autowired
    private UserService userService;

    //1.從每個(gè)請(qǐng)求header獲取token
    //2.調(diào)用前面寫(xiě)的validateToken方法對(duì)token進(jìn)行合法性驗(yàn)證
    //3.解析得到username,并從database取出用戶相關(guān)信息權(quán)限
    //4.把用戶信息(role等)以UserDetail形式放進(jìn)SecurityContext以備整個(gè)請(qǐng)求過(guò)程使用。
    // (例如哪里需要判斷用戶權(quán)限是否足夠時(shí)可以直接從SecurityContext取出去check)
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = getJwtFromRequest(request);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            String username = getUsernameFromJwt(token, authParameters.getJwtTokenSecret());

            UserDetails userDetails = userService.getUserDetailByUserName(username);

            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    userDetails,null,userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);

        } else {
            logger.error(request.getParameter("username") + " :Token is null");
        }
        super.doFilter(request, response, filterChain);
    }

    /**
     * Get Bear jwt from request header Authorization.
     *
     * @param request servlet request.
     * @return token or null.
     */
    private String getJwtFromRequest(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer")) {
            return token.replace("Bearer ", "");
        }
        return null;
    }

    /**
     * Get user name from Jwt, the user name have set to jwt when generate token.
     *
     * @param token jwt token.
     * @param signKey jwt sign key, set in properties file.
     * @return user name.
     */
    private String getUsernameFromJwt(String token, String signKey) {
        return Jwts.parser().setSigningKey(signKey)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}

上面調(diào)用到的getUserDetailByUserName在UserService

    /**
     * Get {@link UserDetails} by user name.
     * @return
     */
    @Transactional
    public UserDetails getUserDetailByUserName(String username){

        User user = this.userRepository.findByUserName(username);

        if(user == null){
            //throw exception inform front end not this user
            throw new UsernameNotFoundException("user + " + username + "not found.");
        }

        List<String> roleList = this.userRepository.queryUserOwnedRoleCodes(username);
        List<GrantedAuthority> authorities = roleList.stream()
                .map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList());

        return new org.springframework.security.core.userdetails
                .User(username,user.getPassword(),authorities);
    }

4.配置HttpSecurity

其他配置說(shuō)明在文章開(kāi)頭提到的另一篇文章中有寫(xiě),這里只說(shuō)新添加的配置
1)添加注解@EnableGlobalMethodSecurity,并設(shè)置prePostEnabled為true(默認(rèn)是false),啟用Spring security的前注解(例如本例用到的@PreAuthorize)
2)把自定義的JwtAuthenticationFilter添加到UsernamePasswordAuthenticationFilter之前。
3)因?yàn)槲覀兪褂昧藅oken,所以session要禁止掉創(chuàng)建和使用,不然會(huì)白白耗掉很多空間,SessionCreationPolicy設(shè)為STATELESS,即永不創(chuàng)建HttpSession并且不會(huì)使用HttpSession去獲取SecurityContext。


WebSecurityConfig.java

WebSecurityConfig.java

5.登陸成功在AuthenticationSuccessHandler返回token給前端

@Service("authenticationSuccessHandler")
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response
            , Authentication authentication) throws IOException {
        logger.info("User: " + authentication.getName() + " Login successfully.");
        this.returnJson(response,authentication);
    }

    private void returnJson(HttpServletResponse response,Authentication authentication) throws IOException {
        response.setStatus(HttpServletResponse.SC_OK);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter()
                .println("{\"tokenType\":\"Bearer\",\"token\": \"" + tokenProvider.createJwtToken(authentication) + "\"}");
    }
}

6.在Controller方法加上具體權(quán)限限制

用@PreAuthorize("hasAuthority('role')"),進(jìn)行方法級(jí)別驗(yàn)證登錄user的是否有足夠的權(quán)限訪問(wèn)該方法,這里舉例用的是admin權(quán)限。

@RestController
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping(value = "/user")
    @PreAuthorize("hasAuthority('admin')")
    public UserView getUserByName(@RequestParam("userName") String userName) {
        return userService.getUserByUserName(userName);
    }
}

hasAuthority在spring中的源碼,主要是在authentication中拿到當(dāng)前user所擁有的role然后再check是否包含有訪問(wèn)這個(gè)方法需要的role。

    public final boolean hasAuthority(String authority) {
        return hasAnyAuthority(authority);
    }

    public final boolean hasAnyAuthority(String... authorities) {
        return hasAnyAuthorityName(null, authorities);
    }
    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = getAuthoritySet();

        for (String role : roles) {
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }

        return false;
    }

7. 測(cè)試

先看看我數(shù)據(jù)庫(kù)的數(shù)據(jù)是這樣的。


user table

role table

user_map_role table
登陸帶admin權(quán)限的用戶,成功獲取資源

1)登錄


login-Sayo

2)請(qǐng)求資源在Header帶上key為Authorization,value為Bearer +token,因?yàn)楫?dāng)前登錄的用戶Sayo在數(shù)據(jù)庫(kù)是帶有admin權(quán)限所以成功獲得數(shù)據(jù)。


request
登錄不帶admin權(quán)限的用戶,無(wú)法獲取資源,返回權(quán)限不夠提示

1)登錄


login-Ly

2)當(dāng)前用戶不帶admin權(quán)限,而該方法配置了需要admin權(quán)限訪問(wèn),請(qǐng)求資源失敗


request
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,711評(píng)論 19 139
  • 原文鏈接:https://docs.spring.io/spring-boot/docs/1.4.x/refere...
    pseudo_niaonao閱讀 4,903評(píng)論 0 9
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,290評(píng)論 6 342
  • 要加“m”說(shuō)明是MB,否則就是KB了. -Xms:初始值 -Xmx:最大值 -Xmn:最小值 java -Xms8...
    dadong0505閱讀 5,075評(píng)論 0 53
  • 看完電影龍蝦,感覺(jué)整個(gè)人都不好了。 本以為是一部烏托邦愛(ài)情片。男主人公來(lái)到一個(gè)酒店,可以在內(nèi)部尋找伴侶,時(shí)間是45...
    子話閱讀 313評(píng)論 0 0

友情鏈接更多精彩內(nèi)容