SpringBoot整合SpringSecurity+JWT實現單點認證

微服務架構,前後端分離目前已成爲互聯網項目開發的業界標準,其核心思想就是前端(APP、小程序、H5頁面等)通過調用後端的API接口,提交及返回JSON數據進行交互。
在前後端分離項目中,首先要解決的就是登錄及授權的問題。微服務架構下,傳統的session認證限制了應用的擴展能力,無狀態的JWT認證方法應運而生,該認證機制特別適用於分佈式站點的單點登錄(SSO)場景

需要了解SpringSecurity安全框架+jwt基本原理

pom 依賴

<!--Security框架-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- jwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.10.6</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.10.6</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.6</version>
</dependency>

yml配置

#jwt
jwt:
  header: authorization
  # 令牌前綴
  token-start-with: bearer
  # 使用Base64對該令牌進行編碼 (制定您的密鑰) 256
  base64-secret: ytkj62672000cnzzabcdefghijlsafsdgasysgfhfgsdhfg
  # 令牌過期時間 此處單位/毫秒
  token-validity-in-seconds: 14400000

JwtSecurityProperties 配置類

package com.yuantiaokj.jwt.jwt;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * ************************************************************
 * Copyright © 2020 遠眺科技 Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: redis-demo
 * @description: jwt配置類
 * @author: cnzz
 * @create: 2020-05-26 08:55
 **/

@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtSecurityProperties {

    /** Request Headers : Authorization */
    private String header;

    /** 令牌前綴,最後留個空格 Bearer */
    private String tokenStartWith;

    /** Base64對該令牌進行編碼 */
    private String base64Secret;

    /** 令牌過期時間 此處單位/毫秒 */
    private Long tokenValidityInSeconds;

    /**返回令牌前綴 */
    public String getTokenStartWith() {
        return tokenStartWith + " ";
    }
}

JwtTokenUtils 工具類

package com.yuantiaokj.jwt.jwt;


import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

/**
 * ************************************************************
 * Copyright © 2020 遠眺科技 Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: redis-demo
 * @description: jwt Token工具類
 * @author: cnzz
 * @create: 2020-05-26 09:01
 **/

@Slf4j
@Component
public class JwtTokenUtils implements InitializingBean {


    private final JwtSecurityProperties jwtSecurityProperties;
    private static final String AUTHORITIES_KEY = "auth";
    private Key key;

    public JwtTokenUtils(JwtSecurityProperties jwtSecurityProperties) {
        this.jwtSecurityProperties = jwtSecurityProperties;
    }

    @Override
    public void afterPropertiesSet() {

        byte[] keyBytes = Decoders.BASE64.decode(jwtSecurityProperties.getBase64Secret());
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }


    public String createToken(String userId,String userName, List<String> permissionList) {

        String newStr = permissionList.stream().collect(Collectors.joining(","));

        return Jwts.builder()
                .claim(AUTHORITIES_KEY, newStr)
                .setId(userId)
                .setSubject(userName)
                .setIssuedAt(new Date())
                .setExpiration(new Date((new Date()).getTime() + jwtSecurityProperties.getTokenValidityInSeconds()))
                .compressWith(CompressionCodecs.DEFLATE)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

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

    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(token)
                .getBody();
        log.debug("我的filter|claims={}", claims);

        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
        log.debug("我的filter|authorities={}", authorities);
        //HashMap map =(HashMap) claims.get("auth");

        //User principal = new User(map.get("user").toString(),map.get("password").toString(), authorities);
        String principal=(String)claims.get("sub");
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(key).parseClaimsJws(authToken);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature.");
            e.printStackTrace();
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token.");
            e.printStackTrace();
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token.");
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            log.info("JWT token compact of handler are invalid.");
            e.printStackTrace();
        }
        return false;
    }

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

WebSecurityConfig 權限配置類

package com.yuantiaokj.jwt.jwt;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
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.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * ************************************************************
 * Copyright © 2020 遠眺科技 Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: redis-demo
 * @description: springSecurity配置類
 * @author: cnzz
 * @create: 2020-05-26 09:08
 **/

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtTokenUtils jwtTokenUtils;

    public WebSecurityConfig(JwtAccessDeniedHandler jwtAccessDeniedHandler, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtTokenUtils jwtTokenUtils) {

        this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
        this.jwtTokenUtils = jwtTokenUtils;

    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                // 禁用 CSRF
                .csrf().disable()

                // 授權異常
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                // 防止iframe 造成跨域
                .and()
                .headers()
                .frameOptions()
                .disable()

                // 不創建會話
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()

                // 放行靜態資源
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/webSocket/**"
                ).permitAll()

                // 放行swagger
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/*/api-docs").permitAll()

                // 放行文件訪問
                .antMatchers("/file/**").permitAll()

                // 放行druid
                .antMatchers("/druid/**").permitAll()

                // 放行OPTIONS請求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

                //允許匿名及登錄用戶訪問
                .antMatchers("/LoginController/loginByUserNameAndPassword",
                        "/LoginController/loginByPhone",
                        "/LoginController/getQrCodeKey",
                        "/LoginController/getUserInfoByKey",
                        "/LoginController/qrCodelogin",
                        "/sysTimeController/getSysTimeMinute"
                ).permitAll()

                // 所有請求都需要認證
                .anyRequest().authenticated();

        // 禁用緩存
        httpSecurity.headers().cacheControl();

        // 添加JWT filter
        httpSecurity
                .apply(new TokenConfigurer(jwtTokenUtils));

    }

    public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

        private final JwtTokenUtils jwtTokenUtils;

        public TokenConfigurer(JwtTokenUtils jwtTokenUtils) {

            this.jwtTokenUtils = jwtTokenUtils;
        }

        @Override
        public void configure(HttpSecurity http) {
            JwtAuthenticationTokenFilter customFilter = new JwtAuthenticationTokenFilter(jwtTokenUtils);
            http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }

}

JwtAuthenticationTokenFilter 過濾器

package com.yuantiaokj.jwt.jwt;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
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;

/**
 * ************************************************************
 * Copyright © 2020 遠眺科技 Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: redis-demo
 * @description: token驗證過濾器
 * @author: cnzz
 * @create: 2020-05-26 09:05
 **/

@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private JwtTokenUtils jwtTokenUtils;

    public JwtAuthenticationTokenFilter(JwtTokenUtils jwtTokenUtils) {
        this.jwtTokenUtils = jwtTokenUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        JwtSecurityProperties jwtSecurityProperties = SpringContextHolder.getBean(JwtSecurityProperties.class);
        String requestRri = httpServletRequest.getRequestURI();
        //獲取request token
        String token = null;
        String bearerToken = httpServletRequest.getHeader(jwtSecurityProperties.getHeader());
        log.debug("從requst獲取的|bearerToken={}",bearerToken);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtSecurityProperties.getTokenStartWith())) {
            token = bearerToken.substring(jwtSecurityProperties.getTokenStartWith().length());
        }

        if (StringUtils.hasText(token) && jwtTokenUtils.validateToken(token)) {
            Authentication authentication = jwtTokenUtils.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), requestRri);
        } else {
            log.debug("no valid JWT token found, uri: {}", requestRri);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);

    }
}

JwtAuthenticationEntryPoint 認證異常

package com.yuantiaokj.jwt.jwt;


import com.alibaba.fastjson.JSON;
import com.yuantiaokj.commonmodule.base.SysRes;
import com.yuantiaokj.commonmodule.code.PubCode;
import com.yuantiaokj.commonmodule.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
 * ************************************************************
 * Copyright © 2020 遠眺科技 Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: redis-demo
 * @description: 認證失敗
 * @author: cnzz
 * @create: 2020-05-26 09:00
 **/

@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {

        log.debug("進入JwtAuthenticationEntryPoint|PC_999001_重新登錄");
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(SysRes.failure(new BizException(PubCode.PC_999001_重新登錄))));

        //throw new BizException(PubCode.PC_999001_重新登錄);
        //response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException==null?"Unauthorized":authException.getMessage());
    }
}

JwtAccessDeniedHandler 權限異常

package com.yuantiaokj.jwt.jwt;

import com.alibaba.fastjson.JSON;
import com.yuantiaokj.commonmodule.base.SysRes;
import com.yuantiaokj.commonmodule.code.PubCode;
import com.yuantiaokj.commonmodule.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * ************************************************************
 * Copyright © 2020 遠眺科技 Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: redis-demo
 * @description: 無權限訪問類
 * @author: cnzz
 * @create: 2020-05-26 08:58
 **/

@Component
@Slf4j
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        log.debug("JwtAccessDeniedHandler|PC_999401_無權限");
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(SysRes.failure(new BizException(PubCode.PC_999401_無權限))));
    }
}

ExceptionController 全局異常處理

package com.yuantiaokj.jwt.globalException;

import com.yuantiaokj.commonmodule.base.SysRes;
import com.yuantiaokj.commonmodule.code.PubCode;
import com.yuantiaokj.commonmodule.exception.BizException;
import com.yuantiaokj.commonmodule.exception.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
 * ************************************************************
 * Copyright © 2020 遠眺科技 Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: redis-demo
 * @description:
 * @author: cnzz
 * @create: 2020-05-28 18:34
 **/

// 統一的異常類進行處理(把默認的異常返回信息改成自定義的異常返回信息)
// 當GlobalContrller拋出HospitalException異常時,將自動找到此類中對應的方法執行,並返回json數據給前臺
@ControllerAdvice
@Slf4j
public class ExceptionController {

    @ResponseBody
    @ExceptionHandler(value = Exception.class)    //異常處理器,處理HospitalException異常
    public SysRes hanlerException(HttpServletRequest request, Exception e) {
        e.printStackTrace();
        return SysRes.exception(e);
    }

    //異常處理器,處理HospitalException異常
    @ResponseBody
    @ExceptionHandler(value = ValidationException.class)
    public SysRes hanlerValidationException(HttpServletRequest request, ValidationException e) {
        e.printStackTrace();
        return SysRes.failure(e);
    }

    //異常處理器,處理HospitalException異常
    @ResponseBody
    @ExceptionHandler(value = BizException.class)
    public SysRes hanlerBizException(HttpServletRequest request, BizException e) {
        e.printStackTrace();
        return SysRes.failure(e);
    }

    //AuthenticationException
    @ResponseBody
    @ExceptionHandler(value = AuthenticationException.class)
    public SysRes hanlerAuthenticationException(HttpServletRequest request, AuthenticationException e) {
       // e.printStackTrace();
        log.debug("進入ExceptionController|PC_999001_重新登錄");
        return SysRes.failure(new BizException(PubCode.PC_999001_重新登錄));
    }

    //AccessDeniedException
    @ResponseBody
    @ExceptionHandler(value = AccessDeniedException.class)
    public SysRes hanlerAccessDeniedException(HttpServletRequest request, AccessDeniedException e) {
        //e.printStackTrace();
        log.debug("進入ExceptionController|PC_999401_無權限");
        return SysRes.failure(new BizException(PubCode.PC_999401_無權限));
    }
}

JwtTokenController jwt流程測試

package com.yuantiaokj.jwt.controller;

import com.alibaba.fastjson.JSON;
import com.yuantiaokj.jwt.jwt.JwtTokenUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * ************************************************************
 * Copyright © 2020 遠眺科技 Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: module-lib
 * @description: 測試
 * @author: cnzz
 * @create: 2020-06-04 16:39
 **/
@RestController
@Slf4j
@RequestMapping("/JwtTokenController")
public class JwtTokenController {
    @Resource
    JwtTokenUtils jwtTokenUtils;

    @GetMapping("/getToken")
    public String getToken() {
        List<String> permissions =new ArrayList<>();
        permissions.add("admin");
        permissions.add("afds");
        String token = jwtTokenUtils.createToken("1223", "abcc", permissions);
        log.info("獲取的|token={}",token);
        return token;
    }

    @RequestMapping("/validPermission")
    @PreAuthorize("hasAnyAuthority('admin')")
    public String validPermission() {
        log.info("進入|validPermission");
        return "OK";
    }


    public static void main(String[] args) {
        List<String> permissions =new ArrayList<>();
        permissions.add("admin");

        log.info(JSON.toJSONString(permissions));
    }
}

後續會打包組件,開箱即用,期待,,,,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章