SpringBoot集成Spring Security+jwt+kaptcha验证(简单实现,可根据实际修改逻辑)

2023-12-13 03:52:48

参考文章

【全网最细致】SpringBoot整合Spring Security + JWT实现用户认证

需求

  • 结合jwt实现登录功能,采用自带/login接口
  • 实现权限控制

熟悉下SpringSecurity

SpringSecurity 采用的是责任链的设计模式,是一堆过滤器链的组合,它有一条很长的过滤器链
集成过程中主要重写过滤器、处理器和配置文件
ps:流程图可以去其他博客看

以下是实现过滤器和处理器

  • LogoutSuccessHandler–登出处理器
  • AuthenticationSuccessHandler–登录认证成功处理器
  • AuthenticationFailureHandler–登录认证失败处理器
  • UserDetailsService–接口十分重要,用于从数据库中验证用户名密码
  • AccessDeniedHandler–用户发起无权限访问请求的处理器 PasswordEncoder–密码验证器
  • OncePerRequestFilter–认证一次请求只通过一次filter,而不需要重复执行

集成开始

引入依赖包

SpringSecurity

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

jwt

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

kaptcha制作验证码

        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>

另外还有一些工具类,reids等依赖包

数据库准备(简单实现,后续根据实际情况设计结构)

准备
user用户表
role角色表
menu菜单表
role_menu角色菜单关系表
user_role用户角色关系表
在这里插入图片描述

kaptcha验证类

DefaultKaptcha 是验证码配置类
KaptchaTextCreator是验证码生成逻辑类,配置在DefaultKaptcha

@Configuration
public class KaptchaConfig {
    /**
     * @Title: CaptchaConfig
     * @Description: 文字验证码
     * @Parameters:
     * @Return
     */
    @Bean(name = "captchaProducer")
    public DefaultKaptcha getKaptchaBean()
    {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        // 是否有边框 默认为true 我们可以自己设置yes,no
        properties.setProperty(KAPTCHA_BORDER, "yes");
        // 验证码文本字符颜色 默认为Color.BLACK
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
        // 验证码图片宽度 默认为200
        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
        // 验证码图片高度 默认为50
        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
        // 验证码文本字符大小 默认为40
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
        // KAPTCHA_SESSION_KEY
        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
        // 验证码文本字符长度 默认为5
        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

    /**
     * @Title: CaptchaConfig
     * @Description: 加法验证码
     * @Parameters:
     * @Return
     */
    @Bean(name = "captchaProducerMath")
    public DefaultKaptcha getKaptchaBeanMath()
    {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        // 是否有边框 默认为true 我们可以自己设置yes,no
        properties.setProperty(KAPTCHA_BORDER, "yes");
        // 边框颜色 默认为Color.BLACK
        properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
        // 验证码文本字符颜色 默认为Color.BLACK
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
        // 验证码图片宽度 默认为200
        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
        // 验证码图片高度 默认为50
        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
        // 验证码文本字符大小 默认为40
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
        // KAPTCHA_SESSION_KEY
        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
        // 验证码文本生成器
        properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.gpd.security.config.KaptchaTextCreator");
        // 验证码文本字符间距 默认为2
        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
        // 验证码文本字符长度 默认为5
        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
        // 验证码噪点颜色 默认为Color.BLACK
        properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
        // 干扰实现类
        properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}
package com.gpd.security.config;
import com.google.code.kaptcha.text.impl.DefaultTextCreator;
import java.util.Random;
public class KaptchaTextCreator extends DefaultTextCreator {

    private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");

    @Override
    public String getText() {
        Integer result = 0;
        /**
         * @Title: KaptchaTextCreator
         * @Description: 生成0-10随机数
         * @Parameters:
         * @Return
         */
        Random random = new Random();
        int x = random.nextInt(10);
        int y = random.nextInt(10);
        /**
         * @Title: KaptchaTextCreator
         * @Description: StringBuilder 用于字符串拼接,但效率更高
         * @Parameters:
         * @Return
         */
        StringBuilder suChinese = new StringBuilder();
        /**
         * @Title: KaptchaTextCreator
         * @Description: 生成0-2随机数,用来生成加减乘除
         * @Parameters:
         * @Return
         */
        int randomoperands = (int) Math.round(Math.random() * 2);
        if (randomoperands == 0)
        {
            result = x * y;
            suChinese.append(CNUMBERS[x]);
            suChinese.append("*");
            suChinese.append(CNUMBERS[y]);
        }
        else if (randomoperands == 1)
        {
            if (!(x == 0) && y % x == 0)
            {
                result = y / x;
                suChinese.append(CNUMBERS[y]);
                suChinese.append("/");
                suChinese.append(CNUMBERS[x]);
            }
            else
            {
                result = x + y;
                suChinese.append(CNUMBERS[x]);
                suChinese.append("+");
                suChinese.append(CNUMBERS[y]);
            }
        }
        else if (randomoperands == 2)
        {
            if (x >= y)
            {
                result = x - y;
                suChinese.append(CNUMBERS[x]);
                suChinese.append("-");
                suChinese.append(CNUMBERS[y]);
            }
            else
            {
                result = y - x;
                suChinese.append(CNUMBERS[y]);
                suChinese.append("-");
                suChinese.append(CNUMBERS[x]);
            }
        }
        else
        {
            result = x + y;
            suChinese.append(CNUMBERS[x]);
            suChinese.append("+");
            suChinese.append(CNUMBERS[y]);
        }
        suChinese.append("=?@" + result);
        return suChinese.toString();
    }
}
获取验证码Controller

有2中验证码返回方式:图片和base64编码,结果是存储在redis上
验证码类型:数字、文字字符串

@Slf4j
@RestController
@RequestMapping("/auth")
@Api(tags = "系统:系统授权接口")
public class AuthenticationController {

    @Resource(name = "captchaProducer")
    private Producer captchaProducer;

    @Resource(name = "captchaProducerMath")
    private Producer captchaProducerMath;

    // 验证码类型
    @Value("${kaptche.captchaType}")
    private String captchaType;
	
	// 验证码有效时间
    @Value("${kaptche.expiration}")
    private Long captchaExpiration;

    @Autowired
    private RedisUtils redisUtil;

    @ApiOperation("获取验证码")
    @GetMapping(value = "/captcha")
    public ResponseEntity Captcha() throws IOException {
        String code = null;
        BufferedImage image = null;

        // 生成验证码
        Map<String, Object> bufferedImage = getBufferedImage(captchaType);
        image = (BufferedImage) bufferedImage.get("image");
        code = (String) bufferedImage.get("code");

        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        ImageIO.write(image, "jpg", os);
        String str = "data:image/jpeg;base64,";
        String base64Img = str + Base64.encode(os.toByteArray());
        String key = UUID.randomUUID().toString();
        Map<Object, Object> result = MapUtil.builder()
                .put("userKey", key)
                .put("captcherImg", base64Img)
                .build();
        redisUtil.set("captcha:"+key, code, captchaExpiration);
        return new ResponseEntity(result, HttpStatus.OK);
    }

    @ApiOperation("获取验证码图片")
    @GetMapping("/getCaptImg")
    public void getCaptImg(HttpServletResponse response, HttpSession session) throws IOException {
        String code = null;
        BufferedImage image = null;

        // 生成验证码
        Map<String, Object> bufferedImage = getBufferedImage(captchaType);
        image = (BufferedImage)bufferedImage.get("image");
        code = (String) bufferedImage.get("code");

        response.setContentType("image/png");
        OutputStream os = response.getOutputStream();
        ImageIO.write(image,"png",os);
    }

    private Map<String, Object> getBufferedImage(String captchaType) {
        String capStr = null, code = null;
        BufferedImage image = null;
        if ("math".equals(captchaType)) {
            String capText = captchaProducerMath.createText();
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            code = capText.substring(capText.lastIndexOf("@") + 1);
            image = captchaProducerMath.createImage(capStr);
        } else if ("char".equals(captchaType)) {
            capStr = code = captchaProducer.createText();
            image = captchaProducer.createImage(capStr);
        }
        Map<String, Object> result = new HashMap<>();
        result.put("code", code);
        result.put("image", image);
        return result;
    }
}

利用postman调用,返回结果去转码,这个校验步骤不要缺,因为有可能生成的base64不能用
在这里插入图片描述

准备一个jwt工具类

有3个功能:生成jwt、解析jwt、判断jwt是否过期
jwt配置

jwt:
  header: Authorization
  # 密钥
  secret: mySecret
  # token 过期时间/毫秒,6小时  1小时 = 3600000 毫秒
  expiration: 21600000
  # 在线用户key
  online: online-token
  # 验证码
  codeKey: code-key
import com.gpd.security.model.JwtUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClock;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;

@Data
@Component
public class JwtUtils implements Serializable {

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

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

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

    private Clock clock = DefaultClock.INSTANCE;

    /**
     *创建token
     * @return
     */
    public String generateToken(Map<String, Object> claims, String subject) {
        return Jwts
                .builder()
                //链式编程 添加头
                .setHeaderParam("typ","JWT")
                .setHeaderParam("alg","HS512")
                //payload 载荷
                .setClaims(claims)
                //主题
                .setSubject(subject)
                //有效期
                .setExpiration(new Date(clock.now().getTime() + expiration))
                //设置id
                .setId(UUID.randomUUID().toString())
                //signature签名
                .signWith(SignatureAlgorithm.HS512, secret)
                //拼接前面三个
                .compact();
    }

    public String generateToken(String username) {

        Date nowDate = new Date();
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(username)
                .setIssuedAt(nowDate)
                .setExpiration(new Date(clock.now().getTime() + expiration))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 校验token
     * @return
     */
    public Boolean validateToken(String token,UserDetails userDetails){
        JwtUser user = (JwtUser) userDetails;
        final Date created = getIssuedAtDateFromToken(token);
        return (!isTokenExpired(token)
                && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
        );
    }

    /**
     * 获取token
     * @param request
     * @return
     */
    public String getToken(HttpServletRequest request){
        final String requestHeader = request.getHeader(tokenHeader);
        if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
            return requestHeader.substring(7);
        }
        return null;
    }

    // 判断JWT是否过期
    public boolean isTokenExpired(Claims claims) {
        return claims.getExpiration().before(new Date());
    }

    private Date getIssuedAtDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getIssuedAt);
    }

    private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    public  Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

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

    private Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

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

统一封装结果Result

我是采用了org.springframework.http自带的ResponseEntity,更简易自己封装一个更好的。以下的代码是用了ResponseEntity来封装结果。
这个是参考的Result统一类

import lombok.Data;
import java.io.Serializable;
@Data
public class Result implements Serializable {
    private int code;
    private String msg;
    private Object data;

    public static Result succ(Object data) {
        return succ(200, "操作成功", data);
    }

    public static Result fail(String msg) {
        return fail(400, msg, null);
    }

    public static Result succ (int code, String msg, Object data) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

    public static Result fail (int code, String msg, Object data) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
}

写登录认证成功、失败处理器LoginSuccessHandler、LoginFailureHandler

自定义一个验证码错误异常
public class CaptchaException extends AuthenticationException {

    public CaptchaException(String msg) {
        super(msg);
    }
}
LoginSuccessHandler 登录成功处理逻辑

onAuthenticationSuccess是登录成功后:更新用户最后登录时间和把用户登录信息写入redis
OnlineUser是独立出来的线上用户实体类
redisUtils工具类网上很多

/**
 * 登录成功处理逻辑
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private RedisUtils redisUtils;

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

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

    @Autowired
    private UserMapper userMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();

        // 生成JWT,并放置到请求头中
        Map<String, Object> claims = new HashMap<>();
        AccountUser accountUser = (AccountUser) authentication.getPrincipal();
        String subject = accountUser.getUsername();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        claims.put("username", subject);
        claims.put("id", accountUser.getUserId());
        claims.put("permissionsJson", JsonUtils.objectToJson(authorities));
        String jwt = jwtUtils.generateToken(claims, subject);

        User user = new User();
        user.setId(accountUser.getUserId());
        user.setLastPasswordResetTime(new Date());
        userMapper.updateById(user);

        redisUtils.set(onlineKey + ":" + subject, saveOnlineUser(subject, jwt), TimeUnit.MILLISECONDS, expiration);
        httpServletResponse.setHeader(jwtUtils.getTokenHeader(), jwt);

        ResponseEntity responseEntity = new ResponseEntity("SuccessLogin", HttpStatus.OK);
        outputStream.write(JsonUtils.objectToJson(responseEntity).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }

    private OnlineUser saveOnlineUser(String username, String jwt) {
        OnlineUser onlineUser = new OnlineUser();
        onlineUser.setUserName(username);
        onlineUser.setToken(jwt);
        return onlineUser;
    }
}
LoginFailureHandler 登录失败处理逻辑
/**
 * 登录失败处理逻辑
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse httpServletResponse, AuthenticationException exception) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();

        String errorMessage = "用户名或密码错误";
        ResponseEntity responseEntity;
        if (exception instanceof CaptchaException) {
            errorMessage = "验证码错误";
            responseEntity = new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
        } else {
            responseEntity = new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
        }
        outputStream.write(JsonUtils.objectToJson(responseEntity).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

JWT认证失败处理器JwtAuthenticationEntryPoint

处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse httpServletResponse, AuthenticationException authException) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();

        Map<Object, Object> result = MapUtil.builder()
                .put("msg", "请先登录")
                .build();
        ResponseEntity responseEntity = new ResponseEntity(result, HttpStatus.BAD_REQUEST);

        outputStream.write(JSONUtil.toJsonStr(responseEntity).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

无权限访问的处理:AccessDenieHandler

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse httpServletResponse, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();

        Map<Object, Object> result = MapUtil.builder()
                .put("msg", accessDeniedException.getMessage())
                .build();
        ResponseEntity responseEntity = new ResponseEntity(result, HttpStatus.BAD_REQUEST);

        outputStream.write(JSONUtil.toJsonStr(responseEntity).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

登出处理器LogoutSuccessHandler

@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
    @Autowired
    JwtUtils jwtUtils;

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        if (authentication != null) {
            new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);
        }

        httpServletResponse.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();

        httpServletResponse.setHeader(jwtUtils.getTokenHeader(), "");

        Map<Object, Object> dataMap = MapUtil.builder()
                .put("msg","SuccessLogout")
                .build();
        ResponseEntity responseEntity = new ResponseEntity(dataMap, HttpStatus.BAD_REQUEST);
		outputStream.write(JSONUtil.toJsonStr(responseEntity).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

密码加密解密:PasswordEncoder

PasswordEncoder 根绝实际的加密情况进行校验

@NoArgsConstructor //生成无参构造方法
public class PasswordEncoder extends BCryptPasswordEncoder {

    // 密码解密加密校验逻辑
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        // 对前端的密码进行加密再跟数据库密码校验(比较简单 建议采取更好的方案)
        String pwd =  EncryptUtils.encryptPassword(rawPassword.toString());
        if (pwd.equals(encodedPassword)){
            return true;
        }
        return false;
    }
}

实现UserDetailsService

从数据库中验证用户名、密码是否正确这种认证方式

创建实体类实现UserDetails

Spring Security在拿到UserDetails之后,会去对比Authentication,Authentication是表单提交的数据

public class AccountUser implements UserDetails {

    private Long userId;

    private static final long serialVersionUID = 540L;
    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 AccountUser(Long userId, String username, String password,Collection<? extends GrantedAuthority> authorities) {
        this(userId, username, password, true, true, true, true,authorities);
    }

    public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = authorities;
    }

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

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

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

    public Long getUserId() {
        return this.userId;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}
自定义一个UserService,UserServiceImpl,UserMapper

实现数据库查询用户信息和权限接口,这里配合了mybatis-plus
用户信息和权限是分开查询了,建议重新封装
UserService

public interface UserService {

    User getByUsername(String userName);

    List<String> getPermissionsById(Long id);
}

UserServiceImpl

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 根据名称获取用户信息
     * @param userName
     * @return
     */
    @Override
    public User getByUsername(String userName) {
        return userMapper.findByRealname(userName);
    }

    /**
     * 根据id获取用户权限
     * @param id
     * @return
     */
    @Override
    public List<String> getPermissionsById(Long id){
        return userMapper.getPermissionsById(id);
    }
}

UserMapper

@Mapper
public interface UserMapper extends BaseMapper<User> {

    @Select("select * from user where user_name = #{realname}")
    User findByRealname(String realname);

    @Select("SELECT DISTINCT m.permission FROM menu m LEFT JOIN role_menu rm ON rm.menu_id=m.id LEFT JOIN user_role ur ON ur.role_id=rm.role_id LEFT JOIN USER u ON u.id=ur.user_id WHERE u.id= #{id}")
    List<String> getPermissionsById(Long id);
}
实现UserDetailServiceImpl

重写loadUserByUsername,从数据库获取用户信息和权限
这里的权限其实只是一个字符串,比如查询权限(tOrder:list),修改权限(tOrder:update)
设计的权限是菜单的权限,根据用户对应的角色,获取所有菜单权限,前端根据权限展示
当然也可以修改成按角色的权限
菜单权限数据例子
在这里插入图片描述

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

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

        User user = userService.getByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名或密码错误");
        }

        // 查询权限
        List<String> permissions = userService.getPermissionsById(user.getId());
        List<GrantedAuthority> grantedAuthoritys = new ArrayList<>();
        if (CollectionUtil.isNotEmpty(permissions)){
            for (String permission:permissions) {
                grantedAuthoritys.add(new SimpleGrantedAuthority(permission));
            }
        }

        AccountUser accountUser = new AccountUser(user.getId(), user.getUsername(), user.getPassword(),grantedAuthoritys);
        return accountUser;
    }
}

实现了上述几个接口,从数据库中验证用户名、密码的过程将由框架帮我们完成,是封装隐藏了,所以不懂Spring Security的人可能会对登录过程有点懵,不知道是怎么判定用户名密码是否正确的

重写OncePerRequestFilter

认证一次请求只通过一次filter,而不需要重复执行。逻辑是登录接口则校验验证码是否正确,然后删除验证码,其他接口则校验jwt

@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

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

    @Autowired
    RedisUtils redisUtils;

    @Autowired
    LoginFailureHandler loginFailureHandler;

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

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

        String url = request.getRequestURI();

        // 如果是登录接口,则进行验证码校验
        if ("/admin-api/login".equals(url) && request.getMethod().equals("POST")) {
            // 校验验证码
            try {
                validate(request);
            } catch (CaptchaException e) {
                // 交给认证失败处理器
                loginFailureHandler.onAuthenticationFailure(request, response, e);
            }
        }


        String jwt = jwtUtils.getToken(request);
        if (null != jwt){
            Claims claim = jwtUtils.getAllClaimsFromToken(jwt);
            if (claim == null) {
                throw new JwtException("token 异常");
            }
            if (jwtUtils.isTokenExpired(claim)) {
                throw new JwtException("token 已过期");
            }
            String username = claim.getSubject(); //用户名称
            OnlineUser onlineUser = (OnlineUser)redisUtils.get(onlineKey+":"+ username);
            if (null != onlineUser  && SecurityContextHolder.getContext().getAuthentication() == null){
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request, response);
    }

    // 校验验证码逻辑
    private void validate(HttpServletRequest httpServletRequest) {
        String code = httpServletRequest.getParameter("code");
        String key = httpServletRequest.getParameter("userKey");

        if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
            throw new CaptchaException("验证码错误");
        }

        if (!code.equals(redisUtils.get("captcha:"+key))) {
            throw new CaptchaException("验证码错误");
        }
        // 若验证码正确,执行以下语句
        // 一次性使用
        redisUtils.remove("captcha:"+key);
    }

}

准备工作完成,配置SecurityConfig

这个配置是结合上面的类写的,设置不拦截登录接口,验证码接口,swagger等接口

@Configuration
@EnableWebSecurity //开启Spring Security的功能
@RequiredArgsConstructor
//prePostEnabled属性决定Spring Security在接口前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true,会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    LoginFailureHandler loginFailureHandler;

    @Autowired
    LoginSuccessHandler loginSuccessHandler;

    @Autowired
    JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter;

    @Autowired
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    JwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Autowired
    UserDetailServiceImpl userDetailService;

    @Autowired
    JwtLogoutSuccessHandler jwtLogoutSuccessHandler;

    /**
     * 白名单请求
     */
    private static final String[] URL_WHITELIST = {
            "/login",
            "/logout",
            "/auth/captcha",
            "/swagger-ui/*",
            "/swagger-resources/**",
            "/v3/api-docs"
    };

    @Bean
    PasswordEncoder PasswordEncoder() {
        return new PasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 支持跨域
                .cors()
                .and()
                // CRSF禁用,因为不使用session 可以预防CRSF攻击
                .csrf()
                .disable()
                // 登录配置
                .formLogin()
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)

                .and()
                .logout()
                .logoutSuccessHandler(jwtLogoutSuccessHandler)

                // 禁用session
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 配置拦截规则
                .and()
                .authorizeRequests()
                .antMatchers(URL_WHITELIST).permitAll()
                .anyRequest().authenticated() // 其余请求都需要过滤
                // 异常处理器
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler);

        // 配置自定义的过滤器
        http.addFilterBefore(jwtAuthorizationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService);
    }

}

测试登录

目前2个用户数据
admin 所有权限
pedro 没有权限
在这里插入图片描述
从头部获取token
在这里插入图片描述
测试一个查询接口,设置了权限,admin账号是有全部权限

@Slf4j
@RestController
@RequestMapping("/api/tOrder")
@Api(value = "订单模块")
public class TOrderController {
    @ApiOperation(value = "查询订单接口")
    @PreAuthorize("@pe.check('tOrder:list')")
    @GetMapping
    public ResponseEntity queryOrder(){
        log.info("查询订单接口");
        Map<String,Object> result = new HashMap<>();
        result.put("1",1);
        return new ResponseEntity(result, HttpStatus.OK);
    }
}

在这里插入图片描述
测试用过,然后测试没有权限的pedro用户
在这里插入图片描述

文章来源:https://blog.csdn.net/weixin_45460120/article/details/134877484
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。