SpringBoot整合SpringSecurity(九)結合JWT(非OAuth2)

序言

jwt有好處也有壞處,好處就是不用在去存這些session了,省空間,做分佈式會話so easy。但是我個人是比較不推薦你使用這個的。我舉例一下幾種缺點

  • 無法滿足註銷場景
  • 無法滿足修改密碼場景
  • 無法滿足token續簽場景

煩人的不能到期,控制到期設置黑名單,還是用到了存儲比如redis,說實話這有點本末倒置,壞處不多說,如果不強求那麼多安全,還是可以使用jwt減少成本,快速開發。

代碼請參考 https://github.com/AutismSuperman/springsecurity-example

思路

只需要加入一個過濾器,保證他在認證過濾器的前面即可。這樣就可以做到在認證前對 token的一個效驗。

這樣就ok了,理解了就開始做。

實踐

切記引入

<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.0</version>
</dependency>

數據

首先準備User實體

@Data
public class User implements UserDetails {

    private Long id;
    private String userName;
    private String password;
    private List<String> roles;


    public User(Long id, String userName, String password, List<String> roles) {
        this.id = id;
        this.userName = userName;
        this.password = password;
        this.roles = roles;
    }

    @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 Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
        return authorities;
    }

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

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

}

然後準備初始化user用戶數據

接口

public interface IUserService {
    User findByUsername(String userName);
}

然後是實現類

@Service
public class UserServiceImpl implements IUserService {

    private static final Set<User> users = new HashSet<>();
	// 密碼123    md5加密 
    static {
        users.add(new User(1L, "fulin", "1dc568b64c0f67e7a86c89a12fa5bd5f", Arrays.asList("admin", "docker")));
        users.add(new User(1L, "xiaohan", "1dc568b64c0f67e7a86c89a12fa5bd5f", Arrays.asList("admin", "docker")));
        users.add(new User(1L, "longlong", "1dc568b64c0f67e7a86c89a12fa5bd5f", Arrays.asList("admin", "docker")));
    }

    @Override
    public User findByUsername(String userName) {
        return users.stream().filter(o -> StringUtils.equals(o.getUsername(), userName)).findFirst().get();
    }
}

userDetailService

@Service
public class UserService implements UserDetailsService {

    final IUserService iUserService;

    public UserService(IUserService iUserService) {
        this.iUserService = iUserService;
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = iUserService.findByUsername(s);
        if (user == null) {
            throw new UsernameNotFoundException("用戶不存在");
        }
        return user;
    }

}

jwt

準備一下jwt的配置

@Data
@ConfigurationProperties(prefix = "security.jwt")
public class SecurityProperties {
    private JwtProperties jwt = new JwtProperties();
}

配置類

/**
 * Jwt的基本配置
 */
@Data
public class JwtProperties {
    /**
     * 默認前面祕鑰
     */
    private String secret = "defaultSecret";

    /**
     * token默認有效期時長,1小時
     */
    private Long expiration = 3600L;
    /**
     * token默認有效期時長,1個半小時
     */
    private Long refreshExpiration = 5400L;

    /**
     * token的唯一標記
     */
    private String md5Key = "randomKey";

    /**
     * GET請求是否需要進行Authentication請求頭校驗,true:默認校驗;false:不攔截GET請求
     */
    private boolean preventsGetMethod = true;
}

配置類別忘了開啓喲

@SpringBootApplication
@EnableConfigurationProperties(SecurityProperties.class)
public class SecuritySimpleJwtApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecuritySimpleJwtApplication.class, args);
    }
}

然後我根據jjwt封裝了一個util方便使用

/**
 * Jwt Util
 */
@Component
public class JwtTokenUtil {

    private final SecurityProperties securityProperties;

    public JwtTokenUtil(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    /**
     * 獲取用戶名從token中
     */
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token).getSubject();
    }

    /**
     * 獲取jwt失效時間
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token).getExpiration();
    }

    /**
     * 獲取私有的jwt claim
     */
    public String getPrivateClaimFromToken(String token, String key) {
        return getClaimFromToken(token).get(key).toString();
    }

    /**
     * 獲取md5 key從token中
     */
    public String getMd5KeyFromToken(String token) {
        return getPrivateClaimFromToken(token, securityProperties.getJwt().getMd5Key());
    }

    /**
     * 獲取jwt的payload部分
     */
    public Claims getClaimFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(generalKey())
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * <pre>
     *  驗證token是否失效
     *  true:過期   false:沒過期
     * </pre>
     */
    public Boolean isTokenExpired(String token) {
        try {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        } catch (ExpiredJwtException expiredJwtException) {
            return true;
        }
    }

    /**
     * 生成token(通過用戶名和簽名時候用的隨機數)
     */
    public String generateToken(String userName, String randomKey) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(securityProperties.getJwt().getMd5Key(), randomKey);
        return doGenerateToken(claims, userName);
    }

    /**
     * 生成token(通過用戶名和簽名時候用的隨機數)
     */
    public String generateRefreshToken(String userName, String randomKey) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(securityProperties.getJwt().getMd5Key(), randomKey);
        return doGenerateRefreshToken(claims, userName);
    }

    /**
     * 由字符串生成加密key
     *
     * @return
     */
    public SecretKey generalKey() {
        byte[] encodedKey = Base64.decodeBase64(securityProperties.getJwt().getSecret());
        return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    }

    /**
     * 生成token
     */
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        final Date createdDate = new Date();
        final Date expirationDate = new Date(createdDate.getTime() + securityProperties.getJwt().getExpiration() * 1000);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, generalKey())
                .compact();
    }

    /**
     * 生成token
     */
    private String doGenerateRefreshToken(Map<String, Object> claims, String subject) {
        final Date createdDate = new Date();
        final Date expirationDate = new Date(createdDate.getTime() + securityProperties.getJwt().getRefreshExpiration() * 1000);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, generalKey())
                .compact();
    }

    /**
     * 獲取混淆MD5簽名用的隨機字符串
     */
    public String getRandomKey() {
        return getRandomString(6);
    }

    /**
     * 獲取隨機位數的字符串
     */
    public String getRandomString(int length) {
        final String base = "abcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }

    /**
     * 刷新token
     *
     * @param token:token
     * @return
     */
    public String refreshToken(String token, String randomKey) {
        String refreshedToken;
        try {
            final Claims claims = getClaimFromToken(token);
            refreshedToken = generateToken(claims.getSubject(), randomKey);
        } catch (Exception e1) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

}

頒發token

都準備齊全了,頒發token呢我們就放在成功處理器去做,這裏爲了讓jwt具有刷新功能,特意準備了一個具備刷新功能的 token refreshToken

@Slf4j
@Component
public class AppAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {


    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        final String randomKey = jwtTokenUtil.getRandomKey();
        String username = ((UserDetails) authentication.getPrincipal()).getUsername();
        log.info("username:{}", username);
        //生產JWT 令牌
        final String token = jwtTokenUtil.generateToken(username, randomKey);
        final String refreshToken = jwtTokenUtil.generateRefreshToken(username, randomKey);
        log.info("登錄成功!");
        ModelMap modelMap = GenerateModelMap.generateMap(HttpStatus.OK.value(), "登陸成功");
        modelMap.put("token", JwtAuthenticationTokenFilter.TOKEN_PREFIX + token);
        modelMap.put("refreshToken", JwtAuthenticationTokenFilter.TOKEN_PREFIX + refreshToken);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(modelMap));
    }
}

jwt過濾器

接下來就是最重要的我們自己定義一個jwt過濾器,這是我自己實現的代碼如下,這裏訪問/refreshToken 重新頒發一個tokenrefreshToken `給前臺

/**
 * JWT過濾器
 * <p>
 * OncePerRequestFilter,顧名思義,
 * 它能夠確保在一次請求中只通過一次filter,而需要重複的執行。
 */
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final String HEADER_NAME = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private UserService userService;


    private AntPathMatcher antPathMatcher = new AntPathMatcher();


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("請求路徑:{},請求方式爲:{}", request.getRequestURI(), request.getMethod());
        if (antPathMatcher.match("/favicon.ico", request.getRequestURI())) {
            log.info("jwt不攔截此路徑:{},請求方式爲:{}", request.getRequestURI(), request.getMethod());
            filterChain.doFilter(request, response);
            return;
        }
        /*
         * get請求是否需要進行Authentication請求頭校驗,true:默認校驗;false:不攔截GET請求
         * 因爲get請求比較特殊
         */
        if (!securityProperties.getJwt().isPreventsGetMethod()) {
            if (Objects.equals(RequestMethod.GET.toString(), request.getMethod())) {
                log.info("jwt不攔截此路徑因爲開啓了不攔截GET請求:{},請求方式爲:{}", request.getRequestURI(), request.getMethod());
                filterChain.doFilter(request, response);
                return;
            }
        }
        /*
         * 排除路徑,並且如果是options請求是cors跨域預請求,設置allow對應頭信息
         * permitUrls可以自定義不需要驗證的url
         */
        String[] permitUrls = {"/authentication"};
        for (String permitUrl : permitUrls) {
            if (antPathMatcher.match(permitUrl, request.getRequestURI())
                    || Objects.equals(RequestMethod.OPTIONS.toString(), request.getMethod())) {
                log.info("jwt不攔截此路徑:{},請求方式爲:{}", request.getRequestURI(), request.getMethod());
                filterChain.doFilter(request, response);
                return;
            }
        }
        // 獲取請求頭Authorization
        String authHeader = request.getHeader(HEADER_NAME);
        if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(TOKEN_PREFIX)) {
            log.error("Authorization的開頭不是Bearer,Authorization===>{}", authHeader);
            responseEntity(response, HttpStatus.UNAUTHORIZED.value(), "暫無權限!");
            return;
        }
        // 截取token
        String authToken = authHeader.substring(TOKEN_PREFIX.length());
        //判斷token是否失效
        if (jwtTokenUtil.isTokenExpired(authToken)) {
            log.info("token已過期!");
            responseEntity(response, HttpStatus.UNAUTHORIZED.value(), "token已過期!");
            return;
        }
        String randomKey = jwtTokenUtil.getMd5KeyFromToken(authToken);
        String username = jwtTokenUtil.getUsernameFromToken(authToken);
        //如果訪問的是刷新Token的請求
        if (antPathMatcher.match("/refreshToken", request.getRequestURI()) && Objects.equals(RequestMethod.POST.toString(), request.getMethod())) {
            final String getRandomKey = jwtTokenUtil.getRandomKey();
            refreshEntity(response, HttpStatus.OK.value(), jwtTokenUtil.generateToken(username, getRandomKey), jwtTokenUtil.refreshToken(authToken, jwtTokenUtil.getRandomKey()));
            return;
        }
        /*
         * 驗證token是否合法
         */
        if (StringUtils.isBlank(username) || StringUtils.isBlank(randomKey)) {
            log.info("username{}或randomKey{} 可能爲null!", username, randomKey);
            responseEntity(response, HttpStatus.UNAUTHORIZED.value(), "暫無權限!");
            return;
        }
        //獲得用戶名信息放入上下文中
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                    request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        // token過期時間
        long tokenExpireTime = jwtTokenUtil.getExpirationDateFromToken(authToken).getTime();

        // token還剩餘多少時間過期
        long surplusExpireTime = (tokenExpireTime - System.currentTimeMillis()) / 1000;
        log.info("Token剩餘時間:" + surplusExpireTime);

        filterChain.doFilter(request, response);

    }

    private void responseEntity(HttpServletResponse response, Integer status, String message) {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(status);
        ModelMap modelMap = GenerateModelMap.generateMap(status, message);
        try {
            response.getWriter().write(JSON.toJSONString(modelMap));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void refreshEntity(HttpServletResponse response, Integer status, String token, String refreshToken) {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(status);
        ModelMap modelMap = new ModelMap();
        modelMap.put("token", JwtAuthenticationTokenFilter.TOKEN_PREFIX + token);
        modelMap.put("refreshToken", JwtAuthenticationTokenFilter.TOKEN_PREFIX + refreshToken);
        try {
            response.getWriter().write(JSON.toJSONString(modelMap));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

加入過濾鏈

最後呢吧過濾器加入到過濾連裏就可以啦

@Configuration
public class ValidateSecurityCoreConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private UserService userService;

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(
                new PasswordEncoder() {

                    @Override
                    public String encode(CharSequence rawPassword) {
                        return MD5Util.encode((String) rawPassword);
                    }

                    @Override
                    public boolean matches(CharSequence rawPassword, String encodedPassword) {
                        return encodedPassword.equals(MD5Util.encode((String) rawPassword));
                    }
                });
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginProcessingUrl("/authentication")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .and()
                .csrf().disable();
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests().antMatchers("/authentication").permitAll();
    }
}

至此呢我們就實現了一個jwt的認證,我只是粗略的做了以下很多都沒考慮進去,大家根據大體思路可以自行擴展(比如token拉黑啥的)。

注意

此外在我的github代碼中還有一套關於redis處理jwt的(感覺有點本末倒置,還不如用sessionId那套配合Spring Session)

本博文是基於springboot2.x 和security 5 如果有什麼不對的請在下方留言。

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