Security 整合JWT篇09

功能

将JWT整合到项目中,具体分2个阶段
1.首次登录进行认证,认证成功就返回token
2.之后的请求携带token进行授权认证,就是说每次授权前都需要重新认证了

1 github: 源码地址

2 security08 子工程

引入jwt依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.yzm</groupId>
        <artifactId>security</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
    </parent>

    <artifactId>security08</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>security08</name>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>com.yzm</groupId>
            <artifactId>common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

项目结构
Security 整合JWT篇09
application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test04?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    password: root

  main:
    allow-bean-definition-overriding: true

mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  type-aliases-package: com.yzm.security08.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3 JwtUtils jwt工具类

package com.yzm.security08.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * JWT工具类
 */
public class JwtUtils implements Serializable {

    private static final long serialVersionUID = 8527289053988618229L;
    /**
     * token头
     * token前缀
     */
    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Basic ";
    /**
     * 用户名称
     */
    public static final String USERNAME = Claims.SUBJECT;
    public static final String PASSWORD = "password";
    /**
     * 权限列表
     */
    public static final String AUTHORITIES = "authorities";
    /**
     * 密钥
     */
    private static final String SECRET = "abcdefg";
    private static final String JWT_SECRET = "7786df7fc3a34e26a61c034d5ec8245d";
    /**
     * 过期时间5分钟
     * 刷新时间2分钟
     */
    public static final long TOKEN_EXPIRED_TIME = 5 * 60 * 1000L;
    public static final long TOKEN_REFRESH_TIME = 2 * 60 * 1000L;

    public static String generateToken(Map<String, Object> claims) {
        return generateToken(claims, 0L);
    }

    /**
     * 生成令牌
     */
    public static String generateToken(Map<String, Object> claims, long expireTime) {
        if (expireTime <= 0L) expireTime = TOKEN_EXPIRED_TIME;

        Map<String, Object> headMap = new HashMap<>();
        headMap.put("typ", "JWT");
        headMap.put("alg", "HS256");

        return Jwts.builder()
                .setHeader(headMap)
                //JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击
                .setId(UUID.randomUUID().toString())
                //.setIssuer("该JWT的签发者,是否使用是可选的")
                //.setSubject("该JWT所面向的用户,是否使用是可选的")
                //.setAudience("接收该JWT的一方,是否使用是可选的")
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                //签发时间(token生成时间)
                .setIssuedAt(new Date())
                //生效时间(在指定时间之前令牌是无效的)
                .setNotBefore(new Date())
                //过期时间(在指定时间之后令牌是无效的)
                .setExpiration(new Date(System.currentTimeMillis() + expireTime))
                //设置签名使用的签名算法和签名使用的秘钥
//                .signWith(SignatureAlgorithm.HS256, JWT_SECRET)
                .signWith(SignatureAlgorithm.HS256, generalKey())
                .compact();
    }

    /**
     * 验证令牌
     */
    public static Claims verifyToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    //签名秘钥
//                    .setSigningKey(JWT_SECRET)
                    .setSigningKey(generalKey())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            // token过期是直接抛出异常的,但仍然可以获取到claims对象
            claims = e.getClaims();
        }
        return claims;
    }

    /**
     * 由密钥生成加密key
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.decodeBase64(SECRET);
//        return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return new SecretKeySpec(encodedKey, SignatureAlgorithm.HS256.getJcaName());
    }

    /**
     * 是否过期
     * true:过期
     * false:未过期
     */
    public static boolean isExpired(String token) {
        Claims claims = verifyToken(token);
        //和当前时间进行对比来判断是否过期
        return claims.getExpiration().before(new Date());
    }

    /**
     * 从令牌中获取用户名
     */
    public static String getUsernameFromToken(String token) {
        Claims claims = verifyToken(token);
        return claims.getSubject();
    }

    /**
     * 获取请求token
     */
    public static String getTokenFromRequest(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_HEADER);
        if (token == null) token = request.getHeader("token");
        if (StringUtils.isBlank(token))
            return null;
        return token.startsWith(TOKEN_PREFIX) ? token.substring(TOKEN_PREFIX.length()) : token;
    }
}

4 第一阶段,登录认证,获取token

首次登录,还是以传统的表单登录模式,所以我们可以直接使用UsernamePasswordAuthenticationFilter来拦截/login进行认证处理,不过我这里自定义了一个JwtAuthenticationFilter,其功能跟UsernamePasswordAuthenticationFilter差不多
JwtAuthenticationFilter 继承 UsernamePasswordAuthenticationFilter 并重写 attemptAuthentication

package com.yzm.security08.config;

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public JwtAuthenticationFilter() {
        super();
    }

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            throw new AuthenticationServiceException("用户名密码错误");
        }
		// 构造未鉴权的JwtAuthenticationToken 对象
        JwtAuthenticationToken authToken = new JwtAuthenticationToken(username, password);
        this.setDetails(request, authToken);
        return this.getAuthenticationManager().authenticate(authToken);
    }

}

JwtAuthenticationToken 继承 UsernamePasswordAuthenticationToken

package com.yzm.security08.config;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {

    public JwtAuthenticationToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    public JwtAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

身份认证 Provider,获取UserDetails对象,校验密码,构造鉴权的Authentication对象

package com.yzm.security08.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;

@Slf4j
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public JwtAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return JwtAuthenticationToken.class.isAssignableFrom(authentication);
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) authentication;
        String username = (String) authenticationToken.getPrincipal();
        String password = (String) authenticationToken.getCredentials();

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails == null) throw new UsernameNotFoundException("账号异常");

        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("凭证异常");
        }

        // 构造鉴权的JwtAuthenticationToken对象
        JwtAuthenticationToken resultToken = new JwtAuthenticationToken(username, password,
                userDetails.getAuthorities());
        resultToken.setDetails(authentication.getDetails());
        return resultToken;
    }
}

登录认证成功之后,返回token

package com.yzm.security08.config;

import com.yzm.common.utils.HttpUtils;
import com.yzm.security08.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;

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

@Slf4j
public class JwtAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        if (authentication instanceof JwtAuthenticationToken) {
            log.info("登录认证");
            JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) authentication;
            String username = (String) authenticationToken.getPrincipal();

            Map<String, Object> map = new HashMap<>();
            map.put(JwtUtils.USERNAME, username);
            String token = JwtUtils.generateToken(map);
            HttpUtils.successWrite(response, token);
        }
    }
}

5 第二阶段,携带token,授权认证

在上面第一阶段,获取到token,那么在之后的请求头中就携带token访问,所以我们需要定义一个Filter来拦截请求头中有 Authorization参数的请求url
JwtAuthorizationFilter 授权过滤器,仿写 AbstractAuthenticationProcessingFilter的

package com.yzm.security08.config;

import com.yzm.security08.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.*;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
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;

public class JwtAuthorizationFilter extends OncePerRequestFilter {

    // 拦截header中带Authorization的请求
    private final RequestMatcher authorizationRequestMatcher = new RequestHeaderRequestMatcher("Authorization");
    private final AuthenticationManager authenticationManager;

    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    protected boolean requiresAuthentication(HttpServletRequest request) {
        return authorizationRequestMatcher.matches(request);
    }

    @Override
    public void afterPropertiesSet() {
        Assert.notNull(authenticationManager, "authenticationManager must be specified");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 请求头是否携带 Authorization
        if (!requiresAuthentication(request)) {
            // 请求头没携带Authorization,可能是登录或匿名访问,直接放行,继续下一个过滤链
            chain.doFilter(request, response);
            return;
        }

        Authentication authResult = null;
        AuthenticationException failed = null;
        try {
            String token = JwtUtils.getTokenFromRequest(request);
            if (StringUtils.isNotBlank(token)) {
            	// 构造未鉴权的JwtAuthorizationToken,认证主体是token,传递给 Provider
                JwtAuthorizationToken authorizationToken = new JwtAuthorizationToken(token, null);
                authorizationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                authResult = this.authenticationManager.authenticate(authorizationToken);
            } else {
                failed = new InsufficientAuthenticationException("JWT is Empty");
            }

        } catch (AuthenticationException e) {
            SecurityContextHolder.clearContext();
            failed = e;
        }

        if (authResult != null) {
            this.successfulAuthentication(request, response, authResult);
        } else {
            // 认证失败
            this.unsuccessfulAuthentication(request, response, failed);
            return;
        }

        // 放行,继续下一个过滤链
        chain.doFilter(request, response);
    }

    protected void unsuccessfulAuthentication(
            HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
          // 认证失败,清除
        SecurityContextHolder.clearContext();
        failureHandler.onAuthenticationFailure(request, response, failed);
    }

    protected void successfulAuthentication(
            HttpServletRequest request, HttpServletResponse response, Authentication authResult)
            throws IOException, ServletException {
         // 认证成功,保存 Authentication 对象到上下文
        SecurityContextHolder.getContext().setAuthentication(authResult);
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
        Assert.notNull(successHandler, "successHandler cannot be null");
        this.successHandler = successHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "failureHandler cannot be null");
        this.failureHandler = failureHandler;
    }

    protected AuthenticationSuccessHandler getSuccessHandler() {
        return this.successHandler;
    }

    protected AuthenticationFailureHandler getFailureHandler() {
        return this.failureHandler;
    }
}

JwtAuthorizationToken 继承 UsernamePasswordAuthenticationToken

package com.yzm.security08.config;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class JwtAuthorizationToken extends UsernamePasswordAuthenticationToken {

    public JwtAuthorizationToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    public JwtAuthorizationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

同样的需要身份验证 Provider,就自定义实现了

package com.yzm.security08.config;

import com.yzm.security08.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.www.NonceExpiredException;

public class JwtAuthorizationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;

    public JwtAuthorizationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return JwtAuthorizationToken.class.isAssignableFrom(authentication);
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        JwtAuthorizationToken authorizationToken = (JwtAuthorizationToken) authentication;
        String token = (String) authorizationToken.getPrincipal();
        if (JwtUtils.isExpired(token)) throw new NonceExpiredException("Token is Expired");

        Claims claims = JwtUtils.verifyToken(token);
        String username = claims.getSubject();
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails == null) throw new UsernameNotFoundException("账号异常");
		// 返回鉴权Authentication对象
        return new JwtAuthorizationToken(token, null, userDetails.getAuthorities());
    }
}

返回鉴权的Authentication对象给JwtAuthorizationFilter,JwtAuthorizationFilter调用成功后处理,存储Authentication对象到上下文中
授权认证成功之后,调用成功处理程序,检查是否需要刷新token,并返回到响应头

package com.yzm.security08.config;

import com.yzm.security08.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;

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

@Slf4j
public class JwtAuthorizationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        if (authentication instanceof JwtAuthorizationToken) {
            log.info("授权认证");
            JwtAuthorizationToken authorizationToken = (JwtAuthorizationToken) authentication;
            String token = (String) authorizationToken.getPrincipal();

            Claims claims = JwtUtils.verifyToken(token);
            long expiration = claims.getExpiration().getTime();
            // 当前时间加上刷新时间大于过期时间,则刷新token
            if (System.currentTimeMillis() + JwtUtils.TOKEN_REFRESH_TIME >= expiration) {
                String username = claims.getSubject();
                Map<String, Object> map = new HashMap<>();
                map.put(JwtUtils.USERNAME, username);
                token = JwtUtils.generateToken(map);
            }

            response.setHeader("Authorization", token);
        }
    }
}

6 一二阶段完成之后,需要配置到Security框架中

package com.yzm.security08.config;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
 * 登录认证
 **/
public class JwtAuthenticationConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public JwtAuthenticationConfigurer(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(http.getSharedObject(AuthenticationManager.class));
        jwtAuthenticationFilter.setAuthenticationSuccessHandler(new JwtAuthenticationSuccessHandler());
        jwtAuthenticationFilter.setAuthenticationFailureHandler(new JwtFailureHandler());

        JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(userDetailsService, passwordEncoder);

        http
                .addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .authenticationProvider(jwtAuthenticationProvider);
    }
}
package com.yzm.security08.config;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
/**
 * 授权认证
 **/
public class JwtAuthorizationConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private final UserDetailsService userDetailsService;

    public JwtAuthorizationConfigurer(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        JwtAuthorizationFilter jwtAuthorizationFilter = new JwtAuthorizationFilter(http.getSharedObject(AuthenticationManager.class));
        jwtAuthorizationFilter.setAuthenticationSuccessHandler(new JwtAuthorizationSuccessHandler());
        jwtAuthorizationFilter.setAuthenticationFailureHandler(new JwtFailureHandler());

        JwtAuthorizationProvider jwtAuthorizationProvider = new JwtAuthorizationProvider(userDetailsService);

        http
                .addFilterAfter(jwtAuthorizationFilter, JwtAuthenticationFilter.class)
                .authenticationProvider(jwtAuthorizationProvider);
    }
}

在SecurityConfig全局配置中使用
既然我们使用了jwt,那么就禁用掉session
也不是页面的表单登录,使用postman请求/login进行登录,所以放行/login
注释配置用户,不使用默认UsernamePasswordAuthenticationFilter,用自定义的

package com.yzm.security08.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;

    public SecurityConfig(@Qualifier("jwtUserDetailsServiceImpl") UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    /**
     * 密码编码器
     * passwordEncoder.encode是用来加密的,passwordEncoder.matches是用来解密的
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

//    /**
//     * 配置用户
//     * 指定默认从哪里获取认证用户的信息,即指定一个UserDetailsService接口的实现类
//     */
//    @Override
//    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        // 从数据库读取用户、并使用密码编码器解密
//        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
//    }

    //配置资源权限规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable() // 关闭CSRF
                .sessionManagement().disable() //禁用session
                .formLogin().disable() //禁用form登录

                // 添加token认证
                .apply(new JwtAuthenticationConfigurer(userDetailsService, passwordEncoder()))
                .and()
                // 添加token授权
                .apply(new JwtAuthorizationConfigurer(userDetailsService))
                .and()

                .exceptionHandling()
                .accessDeniedHandler(new JwtAccessDeniedHandler())
                .and()

                // 退出登录
                .logout()
                .permitAll()
                .and()

                // 访问路径URL的授权策略,如注册、登录免登录认证等
                .authorizeRequests()
                .antMatchers("/", "/home", "/register", "/login").permitAll() //指定url放行
                .anyRequest().authenticated() //其他任何请求都需要身份认证
        ;
    }
}

一二阶段的认证失败都是使用同一处理程序

package com.yzm.security08.config;

import com.yzm.common.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

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

@Slf4j
public class JwtFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("登录失败:" + exception.getMessage());
        String msg = "登录失败";
        if (exception instanceof BadCredentialsException || exception instanceof UsernameNotFoundException) {
            msg = "用户名或密码错误";
        } else if (exception instanceof LockedException) {
            msg = "账号被锁定";
        } else if (exception instanceof AccountExpiredException) {
            msg = "账号过期";
        } else if (exception instanceof CredentialsExpiredException) {
            msg = "凭证过期";
        } else if (exception instanceof DisabledException) {
            msg = "账号被禁用";
        }
        HttpUtils.errorWrite(response, msg);
    }
}

无权访问处理程序

package com.yzm.security08.config;

import com.yzm.common.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;

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

/**
 * 认证过的用户访问无权限资源时的异常
 */
@Slf4j
public class JwtAccessDeniedHandler extends AccessDeniedHandlerImpl {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.info("授权失败:" + accessDeniedException.getMessage());
        HttpUtils.errorWrite(response, accessDeniedException.getMessage());
    }

}

到这里就基本配置完成了,下面进行测试

7 测试

首次登录,进行认证,获取token

Security 整合JWT篇09

携带token,访问
Security 整合JWT篇09

上一篇:JWT的使用


下一篇:复指数信号e^jwt的图像及性质