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

参考文章

【全网最细致】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用户
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/228799.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Centos7部署Graylog5.2日志系统

Graylog5.2部署 Graylog 5.2适配MongoDB 5.x~6.x&#xff0c;MongoDB5.0要求CPU支持AVX指令集。 主机说明localhost部署Graylog&#xff0c;需要安装mongodb-org-6.0、 Elasticsearch7.10.2 参考&#xff1a; https://blog.csdn.net/qixiaolinlin/article/details/129966703 …

基于Python+WaveNet+MFCC+Tensorflow智能方言分类—深度学习算法应用(含全部工程源码)(一)

目录 前言引言总体设计系统整体结构图系统流程图 运行环境Python环境TensorFlow 环境Jupyter Notebook环境Pycharm 环境 相关其它博客工程源代码下载其它资料下载 前言 博主前段时间发布了一篇有关方言识别和分类模型训练的博客&#xff0c;在读者的反馈中发现许多小伙伴对方言…

外贸辅助工具定制的价格范围,别被坑了哟!

随着全球化的不断发展&#xff0c;外贸已成为企业不可或缺的一部分。然而&#xff0c;在外贸过程中&#xff0c;企业往往会遇到各种问题&#xff0c;如语言障碍、文化差异、法规繁琐等&#xff0c;为了解决这些问题&#xff0c;许多企业选择定制外贸辅助工具。 但是&#xff0…

高德地图vue实现自定义标点热力图效果(缩放时展示不同数据)

高德地图插件引入省略。。。样式和vue基础组件省略。。。 如果每个标点没有数值&#xff0c;则可以用点聚合来实现功能下面例子&#xff0c;每个标点会有按市统计的数值&#xff0c;而且缩放一定程度时&#xff0c;需要展示按省统计的标点&#xff0c;因此需要自定义标点样式和…

多相Buck的工作原理

什么是多相Buck电源&#xff1f; 多相电源控制器是一种通过同时控制多个电源相位的设备&#xff0c;以提供稳定的电力供应。相位是指电源中的电流和电压波形。多相控制器的设计旨在最大程度地减小电力转换系统的纹波&#xff0c;并提高整体能效。它通常包含一系列的功率级联&a…

python六子棋ai对战(alpha-beta)剪枝算法

核心代码 def __init__(self): #初始化函数self.num0 #对yi次数self.rows 10 #初始化棋盘10行self.cols 10 # 初始化棋盘10列self.rank6 #阶数 代表六子棋self.empty_board() #清空棋盘self.V 10 #攻击程度self.E10 #防守程度self.depth2 #思考深度…

spring 的概述和入门

​ 我是南城余&#xff01;阿里云开发者平台专家博士证书获得者&#xff01; 欢迎关注我的博客&#xff01;一同成长&#xff01; 一名从事运维开发的worker&#xff0c;记录分享学习。 专注于AI&#xff0c;运维开发&#xff0c;windows Linux 系统领域的分享&#xff01; …

cmake生成表达式

不积小流&#xff0c;无以成江海 <CONFIG:RELEASE> config这个关键字&#xff0c;主要是看CMAKE_BUILD_TYPE这个变量的值是不是和冒号后的一样&#xff0c;一样的话就返回true, 否则就是false. cmake_minimum_required(VERSION 3.10) project(Test) set(CMAKE_CXX_STA…

JVM的内存结构详解「重点篇」

一、JVM虚拟机数据区 虚拟机栈 1、 线程私有 2、 每个方法被执行的时候都会创建一个栈帧用于存储局部变量表&#xff0c;操作栈&#xff0c;动态链接&#xff0c;方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。 3、栈帧: 是用来存储…

HarmonyOS应用开发工具DevEco Studio安装与使用

语雀知识库地址&#xff1a;语雀HarmonyOS知识库 飞书知识库地址&#xff1a;飞书HarmonyOS知识库 知识库内容逐步完善中… 工欲善其事必先利其器&#xff0c;要编写HarmonyOS应用就需要用到官方提供的IDE工具来编写相应的代码。 在鸿蒙开发者官网&#xff0c;其提供了官方的开…

关于什么是 JVM

关于什么是 JVM&#xff0c;看看普通⼈和⾼⼿的回答。 普通人 JVM 就是 Java 虚拟机&#xff0c;是⽤来运⾏我们平时所写的 Java 代码的。优点是它会 ⾃动进⾏内存管理和垃圾回收&#xff0c;缺点是⼀旦发⽣问题&#xff0c;要是不了解 JVM 的运⾏ 机制&#xff0c; 就很难…

企业能用ov多域名https证书

多域名https证书是https数字证书中灵活性较高的一款产品。各个正规CA认证机构旗下的多域名https证书都有同时保护多个域名站点的功能&#xff0c;但是和其它域名https证书不一样的是多域名https证书保护的域名类型比较广。多域名https证书可以保护多个主域名和子域名站点&#…

贪吃的猴子 - 华为OD统一考试(C卷)

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 200分 题解&#xff1a; Java / Python / C 题目描述 一只贪吃的猴子&#xff0c;来到一个果园&#xff0c;发现许多串香蕉排成一行&#xff0c;每串香蕉上有若干根香蕉。每串香蕉的根数由数组numbers给出。猴子获取香…

低多边形3D建模石头材质纹理贴图

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 当谈到游戏角色的3D模型风格时&#xff0c;有几种不同的风格&#xf…

HarmonyOS4.0从零开始的开发教程09页签切换

HarmonyOS&#xff08;七&#xff09;页签切换 List组件和Grid组件的使用 Tabs组件的使用 概述 在我们常用的应用中&#xff0c;经常会有视图内容切换的场景&#xff0c;来展示更加丰富的内容。比如下面这个页面&#xff0c;点击底部的页签的选项&#xff0c;可以实现“首页…

react Hooks实现原理

Fiber 上篇文章fiber简单理解记录了react fiber架构&#xff0c;Hooks是基于fiber链表来实现的。阅读以下内容时建议先了解react fiber。 jsx -> render function -> vdom -> fiber树 -> dom vdom 转 fiber 的过程称为 recocile。diff算法就是在recocile这个过程…

html中一个div中平均分三个盒子

html中一个div中平均分三个盒子 html中一个div中平均分三个盒子&#xff0c;大小自适应&#xff0c;随着界面的大小而改变大小 1、截图展示 2.代码部分 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta …

QT使用SQLite 超详细(增删改查、包括对大量数据快速存储和更新)

QTSQLite 在QT中使用sqlite数据库&#xff0c;有多种使用方法&#xff0c;在这里我只提供几种简单&#xff0c;代码简短的方法&#xff0c;包括一些特殊字符处理。在这里也给大家说明一下&#xff0c;如果你每次要存储的数据量很大&#xff0c;建议使用事务&#xff08;代码中…

老师的就业前景和发展

我常常被问到&#xff0c;“老师的就业前景和发展怎么样&#xff1f;”作为一名老师&#xff0c;我必须承认&#xff0c;教育行业的就业前景和发展并不是特别乐观。但是&#xff0c;这并不意味着没有机会&#xff0c;也不意味着我们不能为自己的未来做出规划。 教育行业的发展趋…

智能无人零售:革新零售消费体验的未来

智能无人零售&#xff1a;革新零售消费体验的未来 在当今数字化时代&#xff0c;智能无人零售正以惊人的速度改变着我们的购物方式和消费体验。这一新兴领域的发展&#xff0c;为消费者带来了前所未有的便利和个性化选择。 智能无人零售是指利用先进的智能技术和自动化系统&…