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

news2024/11/25 11:03:28

参考文章

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2013年全国硕士研究生入学统一考试管理类专业学位联考数学试题——解析版

文章目录 2013 级考研管理类联考数学真题一、问题求解&#xff08;本大题共 15 小题&#xff0c;每小题 3 分&#xff0c;共 45 分&#xff09;下列每题给出 5 个选项中&#xff0c;只有一个是符合要求的&#xff0c;请在答题卡上将所选择的字母涂黑。真题&#xff08;2013-01&…

每天学习一点shell系列(2)—函数的参数传递

参考博客&#xff1a;shell 脚本-10函数_eno_zeng的博客-CSDN博客 $n 或 ${n} &#xff1a;函数内使用 $n 或 ${n} 访问对应的参数, 数字代表参数的前后顺序, $1 代表第一个参数, $2 代表第三个参数, $n 代表第n个参数&#xff1b;当n>10时&#xff0c;需要使用${n}来获取参…

基于 ESP32-S3 的 Walter 开发板

Walter 是一款基于 ESP32-S3 且拥有 5G LTE 连接功能的新型开源开发套件。 近日&#xff0c;比利时公司 DPTechnics BV 推出了一款基于乐鑫 ESP32-S3 且拥有 5G LTE 连接功能的新型开源开发套件。该套件即将在 Crowd Supply 平台上发布&#xff0c;您可以点击此处了解详情。 无…

【Fastadmin】一个完整的轮播图功能示例

目录 1.效果展示&#xff1a; 列表 添加及编辑页面同 2.建表&#xff1a; 3.使用crud一键生成并创建控制器 4.html页面 add.html edit.html index.php 5.js页面 6.小知识点 1.效果展示&#xff1a; 列表 添加及编辑页面同 2.建表&#xff1a; 表名&#xff1a;fa_x…

[足式机器人]Part2 Dr. CAN学习笔记-数学基础Ch0-9阈值选取-机器视觉中应用正态分布和6-sigma

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-数学基础Ch0-9阈值选取-机器视觉中应用正态分布和6-sigma 5M1E——造成产品质量波动的六因素 人 Man Manpower 机器 Machine 材料 Material 方法 Method 测量 Measurment 环境 Envrionment DMAI…

OpenCVForUnity的首部姿态识别功能

手势识别功能 插件名称&#xff1a;OpenCVForUnity 效果 关键代码 HandPoseEstimationMediaPipeExample MediaPipeHandPoseEstimator 第二步&#xff1a; 性能问题&#xff0c;功能是不错&#xff0c;可是一个手部识别的demo&#xff0c;cpu直接飙满了&#xff0c;这哪行。…

Spring基于注解开发

Component的使用 基本Bean注解&#xff0c;主要是使用注解的方式替代原有的xml的<bean>标签及其标签属性的配置&#xff0c;使用Component注解替代<bean>标签中的id以及class属性&#xff0c;而对于是否延迟加载或是Bean的作用域&#xff0c;则是其他注解 xml配置…

【hcie-cloud】【6】华为云Stack网络流量详述【VXLAN简介、华为云Stack节点内部网络结构、华为云Stack网络服务流量走向】、缩略语

文章目录 前言VXLAN简介云数据中心业务对网络的诉求和目标数据中心大二层网络的发展VXLAN简介VXLAN的作用及优势VXLAN网络架构 - Spine-LeafSpine-Leaf架构的基本概念Spine-Leaf架构的优势VXLAN基本概念及工作原理&#xff1a;NVEVXLAN基本概念及工作原理&#xff1a;VTEPVXLAN…

前端:HTML+CSS+JavaScript实现轮播图2

前端&#xff1a;HTMLCSSJavaScript实现轮播图2 1. 和之前版本的区别2. 实现原理3. 针对上述的改进3. 参考代码 1. 和之前版本的区别 之前发布的那篇关于轮播图的文章在这&#xff1a;前端&#xff1a;HTMLCSSJavaScript实现轮播图&#xff0c;只能说存在问题吧&#xff01;比…

Spring Security 6.x 系列(10)—— SecurityConfigurer 配置器及其分支实现源码分析(二)

一、前言 在本系列文章&#xff1a; Spring Security 6.x 系列&#xff08;4&#xff09;—— 基于过滤器链的源码分析&#xff08;一&#xff09; 中着重分析了Spring Security在Spring Boot自动配置、 DefaultSecurityFilterChain和FilterChainProxy 的构造过程。 Spring …

golang学习笔记——爬虫colly入门

文章目录 爬虫第一个爬虫colly爬虫框架colly爬虫示例-爬取图片colly采集器配置CallbacksAdd callbacks to a CollectorCall order of callbacks1. OnRequest2. OnError3. OnResponse4. OnHTML5. OnXML6. OnScraped OnHTML方法 参考资料 爬虫 很多语言都可以写爬虫&#xff0c;…

【蓝桥杯省赛真题50】Scratch消除字母 蓝桥杯scratch图形化编程 中小学生蓝桥杯省赛真题讲解

目录 scratch消除字母 一、题目要求 编程实现 二、案例分析 1、角色分析

Word插件-好用的插件-一键设置字体--大珩助手

常用字体 整理了论文、公文常用字体 整理了常用的论文字体&#xff0c;可一键设置当前节或选择的文字的字体 字体设置 包含字体选择、字体颜色 特殊格式 包含首字下沉、段落分栏、统一宽度、双行合一、上标切换、下标切换、转为全角、转为半角、挖词填空、当前日期、大写金…

[架构之路-259]:目标系统 - 设计方法 - 软件工程 - 软件设计 - 架构设计 - 面向服务的架构SOA与微服务架构(以服务为最小的构建单位)

目录 前言&#xff1a; 二、软件架构层面的复用 三、什么是面向服务的架构SOA 3.1 什么是面向服务的架构 3.2 面向服务架构的案例 3.3 云服务&#xff1a;everything is service一切皆服务 四、什么是微服务架构 4.1 什么是微服务架构 4.2 微服务架构的案例 五、企业…

使用RSA工具进行对信息加解密

我们在开发中需要对用户敏感数据进行加解密&#xff0c;比如密码 这边科普一下RSA算法 RSA是非对称加密算法&#xff0c;与对称加密算法不同;在对称加密中&#xff0c;相同的密钥用于加密和解密数据,因此密钥的安全性至关重要;而在RSA非对称加密中&#xff0c;有两个密钥&…

PandoraFMS 监控软件 任意文件上传漏洞复现

0x01 产品简介 Pandora FMS 是用于监控计算机网络的软件。 Pandora FMS 允许以可视化方式监控来自不同操作系统、服务器、应用程序和硬件系统(例如防火墙、代理、数据库、Web 服务器或路由器)的多个参数的状态和性能。 0x02 漏洞概述 PandoraFMS upload_head_image.php 接…

队列排序:给定序列a,每次操作将a[1]移动到 从右往左第一个严格小于a[1]的元素的下一个位置,求能否使序列有序,若可以,求最少操作次数

题目 思路&#xff1a; 赛时代码&#xff08;先求右起最长有序区间长度&#xff0c;再求左边最小值是否小于等于右边有序区间左端点的数&#xff09; #include<bits/stdc.h> using namespace std; #define int long long const int maxn 1e6 5; int a[maxn]; int n; …

SpringBoot系列之启动成功后执行业务的方法归纳

SpringBoot系列之启动成功后执行业务逻辑。在Springboot项目中经常会遇到需要在项目启动成功后&#xff0c;加一些业务逻辑的&#xff0c;比如缓存的预处理&#xff0c;配置参数的加载等等场景&#xff0c;下面给出一些常有的方法 实验环境 JDK 1.8SpringBoot 2.2.1Maven 3.2…

机器学习之无监督学习:九大聚类算法

今天&#xff0c;和大家分享一下机器学习之无监督学习中的常见的聚类方法。 今天&#xff0c;和大家分享一下机器学习之无监督学习中的常见的聚类方法。 在无监督学习中&#xff0c;我们的数据并不带有任何标签&#xff0c;因此在无监督学习中要做的就是将这一系列无标签的数…

idea使用maven的package打包时提示“找不到符号”或“找不到包”

介绍&#xff1a;由于我们的项目是多模块开发项目&#xff0c;在打包时有些模块内容更新导致其他模块在引用该模块时不能正确引入。 情况一&#xff1a;找不到符号 情况一&#xff1a;找不到包 错误代码部分展示&#xff1a; Failure to find com.xxx.xxxx:xxx:pom:0.5 in …