【智能排班系统】基于SpringSecurity实现登录验证、权限验证

文章目录

SpringSecurity介绍

SpringSecurity是一款专为Java应用程序设计的身份验证和授权框架。它提供了声明式的安全访问控制解决方案,使开发者能够轻松实现用户认证(Authentication)、授权(Authorization)、防止常见安全攻击以及会话管理等功能。作为Spring生态系统的一部分,Spring Security无缝集成于Spring MVC、Spring Boot等项目中,极大地简化了安全相关的开发工作,确保应用程序具备坚实的安全防线

SpringSecurity可以轻松实现细致到按钮级别的权限控制,又因为排班系统有系统管理员、门店管理员、普通员工,每种角色的权限不同,因此非常适合使用SpringSecurity

sss-security实现

依赖

<!-- Spring Security依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><scope>provided</scope>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.14.2</version>
</dependency>

工具类

Jwt工具

用来根据用户信息生成令牌(token),同时可以根据token解析出一些关键信息

package com.dam.utils;import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;import java.util.Date;/*** 生成JSON Web Token的工具类*/
public class JwtUtil {/*** JWT的默认过期时间,单位为毫秒。这里设定为一年(365天)*/private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;/*** 在实际应用中,应使用随机生成的字符串*/private static String tokenSignKey = "dsahdashoiduasguiewu23114";/*** 从给定的JWT令牌中提取指定参数名对应的值。** @param token     需要解析的JWT令牌字符串* @param paramName 要提取的参数名* @return 参数值(字符串形式),如果令牌为空、解析失败或参数不存在,则返回null*/public static String getParam(String token, String paramName) {try {if (StringUtils.isEmpty(token)) {return null;}// 使用提供的密钥解析并验证JWTJws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);// 获取JWT的有效载荷(claims),其中包含了所有声明(参数)Claims claims = claimsJws.getBody();// 提取指定参数名对应的值Object param = claims.get(paramName);// 如果参数值为空,则返回null;否则将其转换为字符串并返回return param == null ? null : param.toString();} catch (Exception e) {// 记录解析过程中的任何异常,并返回nulle.printStackTrace();return null;}}/*** 根据用户信息生成一个新的JWT令牌。** @param userId* @param username* @return*/public static String createToken(Long userId, String username, Long enterpriseId, Long storeId, int userType) {
//        System.out.println("createToken userType:" + userType);// 使用Jwts.builder()构建JWTString token = Jwts.builder()// 设置JWT的主题(subject),此处为常量"AUTH-USER".setSubject("AUTH-USER")// 设置过期时间,当前时间加上预设的过期时间(tokenExpiration).setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))// 有效载荷.claim("userId", userId).claim("username", username).claim("enterpriseId", enterpriseId).claim("storeId", storeId).claim("userType", userType)// 使用HS512算法和指定密钥对JWT进行加密.signWith(SignatureAlgorithm.HS512, tokenSignKey)// 使用GZIP压缩算法压缩JWT字符串,将字符串变成一行来显示.compressWith(CompressionCodecs.GZIP)// 完成构建并生成紧凑格式的JWT字符串.compact();return token;}public static String getUserId(String token) {return getParam(token, "userId");}public static String getUsername(String token) {return getParam(token, "username");}public static String getEnterpriseId(String token) {return getParam(token, "enterpriseId");}public static String getStoreId(String token) {return getParam(token, "storeId");}public static String getUserType(String token) {return getParam(token, "userType");}}

JSON响应工具

ResponseUtil 的作用是为 Spring MVC 应用程序提供一种便捷的方式来构建和发送 JSON 格式的 HTTP 响应

package com.dam.utils;import com.alibaba.fastjson.JSON;
import com.dam.model.result.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;public class ResponseUtil {public static void out(HttpServletResponse response, R r) {ObjectMapper mapper = new ObjectMapper();response.setStatus(HttpStatus.OK.value());response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);try {System.out.println("ResponseUtil r:"+ JSON.toJSONString(r));mapper.writeValue(response.getWriter(), r);} catch (IOException e) {e.printStackTrace();}}
}

加密工具类

本文使用盐值加密来对用户密码进行加密,盐值加密是一种增强密码安全性的技术,主要用于防止密码被轻易破解,特别是在密码数据库遭到泄露的情况下。其核心思想是在密码哈希过程中引入一个额外的、随机生成的值——称为“盐值”,以此来增加密码的唯一性和复杂度,添加盐值有如下作用:

  • 防止彩虹表攻击:彩虹表是一种预先计算好的哈希值与明文密码的映射表,用于快速破解已知哈希算法(如MD5、SHA-1等)生成的密码。通过添加盐值,即使两个用户使用相同的密码,由于盐值不同,其哈希结果也会大相径庭,从而大大削弱彩虹表的有效性。

  • 抵御字典攻击和暴力破解:盐值使得每个用户密码的哈希值都独一无二,即使是最常用的密码,加上随机盐值后,也需要针对特定盐值进行单独破解,显著增加了攻击者的计算成本。

盐值加密的实现直接使用SpringSecurity自带的工具即可

package com.dam.utils;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;/*** 加密工具*/
public class EncryptionUtil {private static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();/*** 盐值MD5加密** @param strSrc* @return*/public static String saltMd5Encrypt(String strSrc) {return passwordEncoder.encode(strSrc);}/*** 判断原密码和加密之后的密码是否相符** @param originalPassword* @param encryptPassword* @return*/public static boolean isSaltMd5Match(String originalPassword, String encryptPassword) {return passwordEncoder.matches(originalPassword, encryptPassword);}public static void main(String[] args) {System.out.println(EncryptionUtil.saltMd5Encrypt("123456"));}
}

用户上下文

用户上下文主要用来记录用户的关键信息,以便同线程共享,无需每次从token中解析,提高效率。使用阿里巴巴的TransmittableThreadLocal库替代标准的java.lang.ThreadLocal,目的是确保在使用线程池或Fork/Join框架等场景下,线程间可以正确地传递(或“传播”)ThreadLocal变量的值。这对于处理跨越多个线程的任务(如异步操作、任务调度等)时保持用户上下文的连续性非常重要。

用户信息实体类

package com.dam.context;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @Author dam* @create 2024/4/2 16:11*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserInfoDTO {private String userId;private String userName;
}

用户上下文

package com.dam.context;import com.alibaba.ttl.TransmittableThreadLocal;import java.util.Optional;/*** @Author dam* @create 2024/4/2 16:12*/
public class UserContext {/*** 定义一个私有的、静态的ThreadLocal变量,类型为UserInfoDTO,用于存储当前线程关联的用户信息*/private static final ThreadLocal<UserInfoDTO> USER_THREAD_LOCAL = new TransmittableThreadLocal<>();/*** 设置用户至上下文** @param user 用户详情信息*/public static void setUser(UserInfoDTO user) {USER_THREAD_LOCAL.set(user);}/*** 获取上下文中用户 ID** @return 用户 ID*/public static String getUserId() {UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();// 使用Optional进行空值安全处理:如果userInfoDTO不为空,则提取其userId属性并返回;否则返回nullreturn Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUserId).orElse(null);}/*** 获取上下文中用户名称** @return 用户名称*/public static String getUsername() {UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUserName).orElse(null);}/*** 清理用户上下文*/public static void removeUser() {// 从ThreadLocal变量中移除当前线程关联的用户信息,释放资源USER_THREAD_LOCAL.remove();}
}

自定义重写

自定义无权限的报错

package com.dam.custom;import com.dam.model.result.R;
import com.dam.utils.ResponseUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** 自定义没有权限的报错信息,默认是报403*/
@Component//交给spring管理
public class CustomAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response,AccessDeniedException accessDeniedException) {// 获取请求的URIString uri = request.getRequestURI();// 获取请求的方法String method = request.getMethod();// 获取当前用户的用户名
//        String username = request.getRemoteUser();// 获取用户的IP地址
//        String ip = request.getRemoteAddr();// 获取用户的浏览器类型
//        String userAgent = request.getHeader("User-Agent");// 构造错误信息String errorMsg = "没有权限访问当前资源:" + uri + " (" + method + ")";ResponseUtil.out(response, R.error(403, errorMsg));}}

自定义密码加密

package com.dam.custom;import com.dam.utils.EncryptionUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;/*** 自定义密码组件*/
@Component//交给spring管理
public class CustomMd5PasswordEncoder implements PasswordEncoder {/*** 指定密码的加密方式** @param rawPassword* @return*/@Overridepublic String encode(CharSequence rawPassword) {return EncryptionUtil.saltMd5Encrypt(rawPassword.toString());}/*** 判断用户所输入的密码和加密之后的密码是否相同** @param rawPassword* @param encodedPassword* @return*/@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {boolean equals = EncryptionUtil.isSaltMd5Match(rawPassword.toString(), encodedPassword);if (equals == true) {return true;} else {System.out.println("登录密码验证不通过,密码错误");
//            System.out.println("原密码,rawPassword:" + rawPassword);
//            System.out.println("原密码加密:" + encrypt);
//            System.out.println("数据库中已加密的密码,encodedPassword:" + encodedPassword);return false;}}
}

自定义用户类

继承security的User,增加一些自己的信息方便后续使用,security的User主要用来存储用户名、密码、权限信息

package com.dam.custom;import com.dam.model.entity.system.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;import java.util.Collection;/*** 自定义用户对象*/
public class CustomUser extends User {/*** 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象*/private UserEntity sysUser;public CustomUser(UserEntity sysUser, Collection<? extends GrantedAuthority> authorities) {super(sysUser.getUsername(), sysUser.getPassword(), authorities);this.sysUser = sysUser;}public UserEntity getSysUser() {return sysUser;}public void setSysUser(UserEntity sysUser) {this.sysUser = sysUser;}}

过滤器

登录过滤器

package com.dam.filter;import com.alibaba.fastjson.JSON;
import com.dam.constant.RedisConstant;
import com.dam.custom.CustomUser;
import com.dam.model.entity.system.UserEntity;
import com.dam.model.enums.ResultCodeEnum;
import com.dam.model.result.R;
import com.dam.model.vo.system.LoginVo;
import com.dam.service.RecordLoginLogService;
import com.dam.utils.JwtUtil;
import com.dam.utils.ResponseUtil;
import com.dam.utils.ip.IpUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {private StringRedisTemplate redisTemplate;/*** 登录日志服务,用于记录用户的登录情况* 方便对系统的用户活跃情况进行统计*/private RecordLoginLogService loginLogService;/*** 构造方法** @param authenticationManager 认证管理器,负责实际的用户身份验证*/public TokenLoginFilter(AuthenticationManager authenticationManager, StringRedisTemplate redisTemplate, RecordLoginLogService sysLoginLogService) {
//        System.out.println("登录验证过滤");this.setAuthenticationManager(authenticationManager);this.redisTemplate = redisTemplate;this.loginLogService = sysLoginLogService;// 不只是可以postthis.setPostOnly(false);// 指定登录接口及提交方式,可以指定任意路径(我们默认的登陆路径)this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/system/login/login", "POST"));}/*** 登录认证,覆盖父类实现** @param req HTTP请求对象* @param res HTTP响应对象* @return 认证后的Authentication对象* @throws AuthenticationException 认证过程中抛出的异常*/@Overridepublic Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)throws AuthenticationException {System.out.println("进行登录认证-----------------------------------------------------------------------------------------");try {// 使用Jackson ObjectMapper从请求流中反序列化登录信息对象LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);System.out.println("loginVo:" + JSON.toJSONString(loginVo));// 判断登录验证码是否正确String redisKey = RedisConstant.Verification_Code + loginVo.getUuid();String verificationCode = redisTemplate.opsForValue().get(redisKey);if (verificationCode == null) {throw new AuthenticationServiceException("验证码已经失效,请刷新之后再重新登录");}if (!verificationCode.toLowerCase().equals(loginVo.getVerificationCode().toLowerCase())) {throw new AuthenticationServiceException("验证码输入不正确");}// 创建UsernamePasswordAuthenticationToken,封装登录信息Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
//            System.out.println("authenticationToken:" + authenticationToken.toString());// 调用父类的authenticate方法,通过认证管理器进行实际的身份验证,会判定登陆密码和数据库密码是否一致Authentication authenticate = this.getAuthenticationManager().authenticate(authenticationToken);System.out.println("登录验证成功");return authenticate;} catch (IOException e) {System.out.println("登录验证失败");throw new RuntimeException(e);}}/*** 登录成功后的处理方法,覆盖父类实现** @param request* @param response* @param chain* @param auth     当前验证对象* @throws IOException* @throws ServletException*/@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication auth) {System.out.println("登录成功,生成token------------------------------------------------------------------------------------------");// 获取当前用户信息CustomUser customUser = (CustomUser) auth.getPrincipal();// 保存权限数据到redisString redisKey = RedisConstant.AUTHORITY_PERMISSION + customUser.getUsername();System.out.println("保存用户权限到redis中,redisKey:" + redisKey);//设置缓存过期时间是十五天redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(customUser.getAuthorities()),15,TimeUnit.DAYS);// 生成tokenUserEntity sysUser = customUser.getSysUser();String token = JwtUtil.createToken(sysUser.getId(), sysUser.getUsername(), sysUser.getEnterpriseId(), sysUser.getStoreId(), sysUser.getType());System.out.println("token:" + token);// 记录登录日志loginLogService.recordLoginLog(customUser.getUsername(), 0, IpUtil.getIpAddress(request), "登录成功", sysUser.getEnterpriseId(), sysUser.getStoreId());// 将token返回给前端Map<String, Object> map = new HashMap<>();map.put("token", token);ResponseUtil.out(response, R.ok().addData("data", map));}/*** 登录失败后的处理方法,覆盖父类实现** @param request  HTTP请求对象* @param response HTTP响应对象* @param e        认证失败异常*/@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,AuthenticationException e) {System.out.println("登录失败------------------------------------------------------------------------------------------");System.out.println("失败原因:" + e.getMessage());// 分析具体失败原因并提供对应的错误信息String errorMessage;if (e instanceof BadCredentialsException) {errorMessage = "用户名或密码错误";} else if (e instanceof DisabledException) {errorMessage = "账户已被禁用,请联系管理员";} else if (e instanceof LockedException) {errorMessage = "账户已被锁定,请联系管理员";} else if (e instanceof AuthenticationServiceException) {errorMessage = "认证服务异常,请稍后重试";} else {errorMessage = "登录失败";}ResponseUtil.out(response, R.error(ResultCodeEnum.DATA_ERROR.getCode(), errorMessage));}
}

权限过滤器

这段代码定义了一个名为TokenAuthenticationFilter的类,它继承自Spring Security的OncePerRequestFilter,用于处理每个HTTP请求,解析并验证请求头中的Token,以及将认证信息放入Spring Security的上下文中。

package com.dam.filter;import com.alibaba.fastjson.JSON;
import com.dam.constant.RedisConstant;
import com.dam.context.UserContext;
import com.dam.context.UserInfoDTO;
import com.dam.model.enums.ResultCodeEnum;
import com.dam.model.result.R;
import com.dam.utils.JwtUtil;
import com.dam.utils.ResponseUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;/*** 认证解析token过滤器* OncePerRequestFilter:每次请求都要过滤*/
public class TokenAuthenticationFilter extends OncePerRequestFilter {private StringRedisTemplate redisTemplate;public TokenAuthenticationFilter(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}/*** 重写OncePerRequestFilter的doFilterInternal方法,处理每个请求* @param request* @param response* @param chain* @throws IOException* @throws ServletException*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {System.out.println("权限验证过滤");//        logger.info("uri:" + request.getRequestURI());
//        System.out.println("request.getRequestURI():"+request.getRequestURI());//如果是登录接口,直接放行if ("/system/login/login".equals(request.getRequestURI())) {chain.doFilter(request, response);return;}// 调用getAuthentication方法尝试从请求中获取有效的Token并解析出认证信息UsernamePasswordAuthenticationToken authentication = getAuthentication(request);if (null != authentication) {// --if--如果获取到有效的认证信息
//            System.out.println("request:" + request.toString());
//            System.out.println("response:" + response.toString());
//            System.out.println("authentication:" + authentication.toString());// 将认证信息放入Spring Security的SecurityContextHolder中,以便后续请求链中使用SecurityContextHolder.getContext().setAuthentication(authentication);// 继续执行过滤链中的其他过滤器和目标处理器chain.doFilter(request, response);} else {// 如果未能获取到有效的认证信息,返回失败响应ResponseUtil.out(response, R.ok().addData("data", ResultCodeEnum.PERMISSION));}}/*** 看看是否有token,根据token是否可以获取到用户,获取不到再进行账号密码登录** @param request* @return*/private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {System.out.println("获取权限信息------------------------------------------------------------------------------------------");// token置于header里String token = request.getHeader("token");
//        logger.info("token:" + token);if (!StringUtils.isEmpty(token)) {String username = JwtUtil.getUsername(token);
//            logger.info("username:" + username);if (!StringUtils.isEmpty(username)) {// 获取授权信息String redisKey = RedisConstant.AUTHORITY_PERMISSION + username;// 权限字符串String authoritiesString = redisTemplate.opsForValue().get(redisKey);if (authoritiesString == null) {return null;}// 解析权限字符串为具体的权限集合List<Map> mapList = JSON.parseArray(authoritiesString, Map.class);List<SimpleGrantedAuthority> authorities = new ArrayList<>();for (Map map : mapList) {authorities.add(new SimpleGrantedAuthority((String) map.get("authority")));}// 存储用户上下文信息String userId = JwtUtil.getUserId(token);UserContext.setUser(UserInfoDTO.builder().userId(userId).userName(username).build());// 构建并返回UsernamePasswordAuthenticationToken对象,包含用户名、空密码(此处无需密码,因为已通过Token验证)和权限列表return new UsernamePasswordAuthenticationToken(username, null, authorities);}}// 如果未能成功解析Token或获取权限信息,返回nullreturn null;}
}

Service

登录Service

package com.dam.service;import com.dam.model.entity.system.LoginLogEntity;public interface RecordLoginLogService {/*** 记录登录信息** @param username 用户名* @param status   状态* @param ipaddr   ip* @param message  消息内容* @return*/void recordLoginLog(String username, Integer status, String ipaddr, String message,Long enterpriseId,Long storeId);LoginLogEntity getById(Long id);}

配置类

里面有一些接口当时偷懒没有在数据库里面配置相应的权限,为了开发的时候方便测试,将其放在了忽略接口路径,后续需要修改。

package com.dam.config;import com.dam.configuration.IpFlowControlConfiguration;
import com.dam.custom.CustomAccessDeniedHandler;
import com.dam.custom.CustomMd5PasswordEncoder;
import com.dam.filter.IpFlowLimitFilter;
import com.dam.filter.TokenAuthenticationFilter;
import com.dam.filter.TokenLoginFilter;
import com.dam.service.RecordLoginLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity //开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别的安全注解(如@PreAuthorize, @PostAuthorize)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowired
//    @Qualifier("systemUserDetailsServiceImpl") // 指定实现类private UserDetailsService userDetailsService;@Autowiredprivate CustomMd5PasswordEncoder customMd5PasswordEncoder;@Autowiredprivate StringRedisTemplate redisTemplate;@Autowired
//    @Qualifier("systemRecordLoginLogServiceImpl") // 指定实现类private RecordLoginLogService loginLogService;@Autowiredprivate CustomAccessDeniedHandler customAccessDeniedHandler;@Autowiredprivate IpFlowControlConfiguration ipFlowControlConfiguration;/*** 创建并返回一个AuthenticationManager实例,此方法由父类WebSecurityConfigurerAdapter提供** @return* @throws Exception*/@Bean@Overrideprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护http// 自定义没有权限时的报错信息.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler)// 关闭CSRF(跨站请求伪造)防护.and().csrf().disable()// 开启跨域以便前端调用接口(网关已经做了全局跨域)//.cors().and()// 设置访问控制规则.authorizeRequests()// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的(在下面统一配置了)//.antMatchers("/system/login/login").permitAll()// 这里意思是其它所有接口需要认证才能访问.anyRequest().authenticated()// 添加自定义过滤器,按照顺序依次执行.and()// TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。.addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)// IpFlowLimitFilter:在TokenAuthenticationFilter之前,进行IP流量限制.addFilterBefore(new IpFlowLimitFilter(redisTemplate, ipFlowControlConfiguration), TokenAuthenticationFilter.class)// TokenLoginFilter:处理登录请求,使用AuthenticationManager进行认证,并记录登录日志.addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate, loginLogService));// 禁用session (采用无状态会话管理,适用于基于Token的身份验证)http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 指定 UserDetailService 和 加密器auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);}/*** 配置哪些请求不拦截* 重写configure(WebSecurity)方法* 使用web.ignoring().antMatchers(...)指定一系列接口路径,* 这些路径的请求将不会经过Spring Security的过滤链,即不受安全约束。** @param web* @throws Exception*/@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**","/webjars/**", "/v2/**","/swagger-ui.html/**","/doc.html","/system/login/sendMailCode","/system/login/usernameCheck/**","/system/login/regist","/system/login/enterpriseRegister","/system/user/getByOpenid","/system/user/getUserEntityByToken","/system/user/bindWechat","/system/login/generateVerificationCode","/api/ucenter/wx/callback","/scheduling/imserver/**",//定时任务需要访问"/scheduling/schedulingdate/judgeOneDateIsRest","/scheduling/shiftuser/listStaffWorkDtoByWorkDate","/system/user/getUserIdAndMailMapByUserIdList","/system/user/listUserEntityByStoreId","/system/menu/storeAuthoritiesToRedis","/thirdParty/oss/policy","/thirdParty/mail/send","/api/ucenter/wx/**","/thirdParty/mail/send");}
}

说明

登录验证

使用了SpringSecurity之后,不需要再自己实现登录方法,因为在上面已经完成了验证码校验、密码校验

权限验证

系统的权限控制方式是:将菜单权限绑定到角色中,然后再将角色分配给用户。在登录成功之后,将用户对应的权限标识查询出来并存储到Redis中,当用户访问需要权限的接口时,SpringSecurity会从Redis中获取用户有的权限标识,然后判断用户是否有接口对应权限,没有则报没有权限错误

在这里插入图片描述
那么怎么给接口做权限控制呢,实现非常简单,只需要在接口上面添加注解和相应的权限标识,如@PreAuthorize("hasAuthority('bnt.sysMenu.list')")

/*** 列表*/
@RequestMapping("/list")
@PreAuthorize("hasAuthority('bnt.sysMenu.list')")
public R list(@RequestParam Map<String, Object> params) {PageUtils page = menuService.queryPage(params);return R.ok().addData("page", page);
}

IP流量限制

如果看过我的代码的同学,可以还有一些IP流量限制的代码我没有讲解,感兴趣的同学可以查看【智能排班系统】基于Redis的increment命令和lua脚本实现IP限流

sss-system模块实现

Service实现

登录日志实现类

package com.dam.service.impl.security;import com.dam.dao.LoginLogDao;
import com.dam.model.entity.system.LoginLogEntity;
import com.dam.service.RecordLoginLogService;
import com.dam.utils.ServletUtils;
import eu.bitwalker.useragentutils.UserAgent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;//@Service("systemRecordLoginLogServiceImpl")
@Service
public class RecordLoginLogServiceImpl implements RecordLoginLogService {@Autowiredprivate LoginLogDao loginLogDao;@Overridepublic void recordLoginLog(String username, Integer status, String ipaddr, String message, Long enterpriseId, Long storeId) {LoginLogEntity sysLoginLog = new LoginLogEntity();sysLoginLog.setUsername(username);sysLoginLog.setIpaddr(ipaddr);sysLoginLog.setMsg(message);// 日志状态sysLoginLog.setStatus(status);/// 获取用户的浏览器和操作系统final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));// 获取客户端操作系统String os = userAgent.getOperatingSystem().getName();sysLoginLog.setOs(os);// 获取客户端浏览器String browser = userAgent.getBrowser().getName();sysLoginLog.setBrowser(browser);/// 存储用户的企业 门店信息if (enterpriseId != null) {sysLoginLog.setEnterpriseId(enterpriseId);}if (storeId != null) {sysLoginLog.setStoreId(storeId);}loginLogDao.insert(sysLoginLog);}//    @Override
//    public IPage<LoginLogEntity> selectPage(Page<LoginLogEntity> pageParam, LoginLogQueryVo sysLoginLogQueryVo) {
//        //获取条件值
//        String username = sysLoginLogQueryVo.getUsername();
//        String createTimeBegin = sysLoginLogQueryVo.getCreateTimeBegin();
//        String createTimeEnd = sysLoginLogQueryVo.getCreateTimeEnd();
//        //封装条件
//        QueryWrapper<LoginLogEntity> wrapper = new QueryWrapper<>();
//        if (!StringUtils.isEmpty(username)) {
//            wrapper.like("username", username);
//        }
//        if (!StringUtils.isEmpty(createTimeBegin)) {
//            wrapper.ge("create_time", createTimeBegin);
//        }
//        if (!StringUtils.isEmpty(createTimeBegin)) {
//            wrapper.le("create_time", createTimeEnd);
//        }
//        //调用mapper方法
//        IPage<LoginLogEntity> pageModel = loginLogDao.selectPage(pageParam, wrapper);
//        return pageModel;
//    }@Overridepublic LoginLogEntity getById(Long id) {return loginLogDao.selectById(id);}//    @Override
//    public PageUtils queryPage(Map<String, Object> params) {
//        IPage<LoginLogEntity> page = this.page(
//                new Query<LoginLogEntity>().getPage(params),
//                new QueryWrapper<LoginLogEntity>().orderByDesc("create_time")
//        );
//
//        return new PageUtils(page);
//    }
}

这段代码定义了一个名为UserDetailsServiceImpl的类,实现了Spring Security的UserDetailsService接口,用于根据用户名加载用户详细信息,包括用户权限

package com.dam.service.impl.security;import com.dam.custom.CustomUser;
import com.dam.model.entity.system.UserEntity;
import com.dam.model.enums.system.UserCodeEnum;
import com.dam.service.MenuService;
import com.dam.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserService sysUserService;@Autowiredprivate MenuService menuService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {System.out.println("根据用户名查询用户授权信息----------------------------------------------------------------------------");// 通过UserService根据用户名查询用户信息UserEntity sysUser = sysUserService.getUserInfoByUsername(username);// 用户信息不存在时抛出异常if (null == sysUser) {throw new UsernameNotFoundException("用户名不存在,请检查输入是否错误");}// 判断用户状态,如被禁用则抛出异常if (sysUser.getStatus().intValue() == UserCodeEnum.STATUS_BAN.getCode().intValue()) {throw new RuntimeException("账号已被禁用,请咨询管理员");}// 根据userId查询操作权限List<String> userPermsList = menuService.getUserButtonList(sysUser.getId());System.out.println("用户可操作按钮,userPermsList:" + userPermsList);// 转化成security要求的格式数据List<SimpleGrantedAuthority> authorities = new ArrayList<>();for (String perm : userPermsList) {authorities.add(new SimpleGrantedAuthority(perm.trim()));}return new CustomUser(sysUser, authorities);}}

UserDetailsService实现类

package com.dam.service.impl.security;import com.dam.custom.CustomUser;
import com.dam.model.entity.system.UserEntity;
import com.dam.model.enums.system.UserCodeEnum;
import com.dam.service.MenuService;
import com.dam.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserService sysUserService;@Autowiredprivate MenuService menuService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {System.out.println("根据用户名查询用户授权信息----------------------------------------------------------------------------");// 通过UserService根据用户名查询用户信息UserEntity sysUser = sysUserService.getUserInfoByUsername(username);// 用户信息不存在时抛出异常if (null == sysUser) {throw new UsernameNotFoundException("用户名不存在,请检查输入是否错误");}// 判断用户状态,如被禁用则抛出异常if (sysUser.getStatus().intValue() == UserCodeEnum.STATUS_BAN.getCode().intValue()) {throw new RuntimeException("账号已被禁用,请咨询管理员");}// 根据userId查询操作权限List<String> userPermsList = menuService.getUserButtonList(sysUser.getId());System.out.println("用户可操作按钮,userPermsList:" + userPermsList);// 转化成security要求的格式数据List<SimpleGrantedAuthority> authorities = new ArrayList<>();for (String perm : userPermsList) {authorities.add(new SimpleGrantedAuthority(perm.trim()));}return new CustomUser(sysUser, authorities);}}

测试

读者们可能看到上面的代码,整个人是一脸懵逼,这么多类,这么多方法,究竟是怎么执行的,下面我通过测试给出了调用链路,大家可以跟着顺序来理解上面的代码

登录失败测试

模拟密码错误,代码的执行顺序如下:

>>>>> 调用 TokenAuthenticationFilter 的 doFilterInternal
>>>>> 调用 TokenLoginFilter 的 attemptAuthentication
>>>>> 调用 UserDetailsServiceImpl 的 loadUserByUsername
>>>>> 调用 CustomMd5PasswordEncoder 的 matches
>>>>> 调用 TokenLoginFilter 的 unsuccessfulAuthentication

登录成功测试

输入正确密码,代码的执行顺序如下:

>>>>> 调用 TokenAuthenticationFilter 的 doFilterInternal
>>>>> 调用 TokenLoginFilter 的 attemptAuthentication
>>>>> 调用 UserDetailsServiceImpl 的 loadUserByUsername
>>>>> 调用 CustomMd5PasswordEncoder 的 matches
>>>>> 调用 TokenLoginFilter 的 successfulAuthentication
>>>>> 调用 RecordLoginLogServiceImpl 的 recordLoginLog

其他请求测试

发起一个除了登录之外的请求,代码的执行顺序如下:

>>>>> 调用 TokenAuthenticationFilter 的 doFilterInternal
>>>>> 调用 TokenAuthenticationFilter 的 getAuthentication

其他建议

如果对用户权限标识存储有什么不理解的地方,可以参考【智能排班系统】数据库设计的菜单表角色表用户表角色菜单中间表用户角色中间表

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

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

相关文章

【.Net】Polly

文章目录 概述服务熔断、服务降级、服务限流、流量削峰、错峰、服务雪崩Polly的基本使用超时策略悲观策略乐观策略 重试策略请求异常响应异常 降级策略熔断策略与策略包裹&#xff08;多种策略组合&#xff09; 参考 概述 Polly是一个被.NET基金会支持认可的框架&#xff0c;同…

在线监测系统在水厂水质管理工程中的应用与研究

【摘要】&#xff1a;随着水厂水质管理技术和管理水平的提升&#xff0c;达到了在线监测系统通过监测数据的反馈&#xff0c;及时发现问题&#xff0c;快速处理事故&#xff0c;优化了水资源的利用率&#xff0c;提高了供水系统的稳定性和安全性&#xff0c;从而有效地提高供水…

FX110网:菲律宾 eToro 发起人面临最高 21 年监禁的风险

任何在菲律宾推广 eToro 的“推销员、经纪人、经销商或代理商”将面临 500 万比索&#xff08;约 88,500 美元&#xff09;的罚款或最高 21 年的监禁&#xff0c;或两者并罚。据当地监管机构称&#xff0c;这是因为“无权在菲律宾向公众出售或发行证券”。 菲律宾证券交易委员会…

Springboot相关知识-图片描述(学习笔记)

学习java过程中的一些笔记&#xff0c;觉得比较重要就顺手记录下来了~ 目录 一、前后端请求1.前后端交互2.简单传参3.数组集合传参4.日期参数5.Json参数6.路径参数7.响应数据8.解析xml文件9.统一返回类10.三层架构11.分层解耦12.Bean的声明13.组件扫描14.自动注入 一、前后端请…

时序预测 | Matlab实现CPO-BiLSTM【24年新算法】冠豪猪优化双向长短期记忆神经网络时间序列预测

时序预测 | Matlab实现CPO-BiLSTM【24年新算法】冠豪猪优化双向长短期记忆神经网络时间序列预测 目录 时序预测 | Matlab实现CPO-BiLSTM【24年新算法】冠豪猪优化双向长短期记忆神经网络时间序列预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现CPO-BiLST…

由两个线路驱动器、两个线路接收器和双电荷泵电路组成的芯片D3232,主要用于工控主板、新能源充电桩等众多涉及RS232通讯的产品中

一、应用领域 D3232芯片主要用于工控主板、工业控制器、程序烧录下载器、仿真器、新能源充电桩等众多涉及RS232通讯的产品。 二、基本特性 D3232芯片由两个线路驱动器、两个线路接收器和双电荷泵电路组成&#xff0c;具有HBM>15kV、CDM>2kV的ESD保护能力&#xff0c;并且…

在线视频教育平台|基于Springboot的在线视频教育平台系统设计与实现(源码+数据库+文档)

在线视频教育平台目录 基于Springboot的在线视频教育平台系统设计与实现 一、前言 二、系统设计 三、系统功能设计 1、前台&#xff1a; 2、后台 用户功能模块 教师功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&a…

Vue - 3( 15000 字 Vue 入门级教程)

一&#xff1a;初识 Vue 1.1 收集表单数据 收集表单数据在Vue.js中是一个常见且重要的任务&#xff0c;它使得前端交互变得更加灵活和直观。 Vue中&#xff0c;我们通常使用v-model指令来实现表单元素与数据之间的双向绑定&#xff0c;从而实现数据的收集和更新。下面总结了…

Java Spring IoCDI :探索Java Spring中控制反转和依赖注入的威力,增强灵活性和可维护性

&#x1f493; 博客主页&#xff1a;从零开始的-CodeNinja之路 ⏩ 收录文章&#xff1a;Java Spring IoC&DI :探索Java Spring中控制反转和依赖注入的威力,增强灵活性和可维护性 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 前提小知识:高内…

LeetCode-78. 子集【位运算 数组 回溯】

LeetCode-78. 子集【位运算 数组 回溯】 题目描述&#xff1a;解题思路一&#xff1a;回溯&#xff0c;回溯三部曲解题思路二&#xff1a;0解题思路三&#xff1a;0 题目描述&#xff1a; 给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的 子…

【SpringCloud】Nacos 注册中心

目 录 一.认识和安装 Nacos1.Windows安装1. 下载安装包2. 解压3. 端口配置4. 启动5. 访问 2.Linux安装1. 安装JDK2. 上传安装包3. 解压4. 端口配置5. 启动 二.服务注册到 nacos1. 引入依赖2. 配置 nacos 地址3. 重启 三.服务分级存储模型1. 给 user-service 配置集群2. 同集群优…

JavaWeb前端基础(HTML CSS JavaScript)

本文用于检验学习效果&#xff0c;忘记知识就去文末的链接复习 1. HTML 1.1 HTML基础 结构 头<head>身体<body> 内容 图片<img>段落<p>图标<link> 标签 单标签双标签 常用标签 div&#xff1a;分割块span&#xff1a;只占需要的大小p&…

从三个维度看,你的企业是否需要引入精益管理咨询?

在快速变化的商业环境中&#xff0c;企业不断寻求提升自身运营效率和竞争力的方法。其中&#xff0c;精益管理作为一种追求卓越、消除浪费的管理理念&#xff0c;被越来越多的企业所认可。但是&#xff0c;如何判断自己的组织是否需要进行精益企业管理咨询呢&#xff1f;天行健…

【漏洞复现】通天星CMSV6车载视频监控平台FTP匿名访问

Nx01 产品简介 通天星车载视频监控平台软件拥有多种语言版本&#xff0c;应用于公交车车载视频监控、校车车载视频监控、大巴车车载视频监控、物流车载监控、油品运输车载监控等公共交通上。 Nx02 漏洞描述 通天星车载视频监控平台安装完毕后会默认开放端口2121作为ftp服务使用…

多语言婚恋交友APP开发的关键成功因素

随着移动互联网的快速发展&#xff0c;多语言婚恋交友APP的需求和发展逐渐成为了一个备受关注的话题。在全球范围内&#xff0c;人们希望通过移动应用来寻找爱情、建立关系和拓展社交圈子&#xff0c;因此开发一款具有全球影响力的多语言婚恋交友APP成为了许多开发者的目标。针…

Java实现Excel百万级数据的导入(约30s完成)

前言 在遇到大数据量excel&#xff0c;50MB大小或数百万级别的数据读取时&#xff0c;使用常用的POI容易导致读取时内存溢出或者cpu飙升。 本文讨论的是针对xlsx格式的excel文件上传&#xff0c;采用com.monitorjbl.xlsx.StreamingReader 。 什么是StreamReader? StreamReader…

开源低代码平台概况和说明推荐

开源低代码平台是一类允许开发者通过图形化界面和预构建的代码块&#xff0c;而非传统的手动编程方式&#xff0c;来创建应用程序的工具。这些平台通常提供了丰富的功能和特性&#xff0c;帮助开发者更加高效地进行应用开发。 开源低代码平台的概况可以总结为以下几点&#xf…

【C++】背包问题

目录 背包问题01 背包背包不装满问题背包必须满问题 完全背包 背包问题 背包问题属于动态规划的一类题型 01 背包 背包不装满问题 背包必须满问题 #include <iostream> using namespace std; const int N 1010; #include <vector> int main() {int n , V;int v[…

【Ambari】Ansible自动化部署大数据集群

目录 一&#xff0e;版本说明和介绍信息 1.1 大数据组件版本 1.2 Apache Components 1.3 Databases支持版本 二&#xff0e;安装包上传和说明 三&#xff0e;服务器基础环境配置 3.1global配置修改 3.2主机名映射配置 3.3免密用户名密码配置 3.4 ansible安装 四. 安…

2024.4.1-[作业记录]-day06-认识 CSS(三大特性、引入方式)

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; day06-认识 CSS(三大特性、引入方式) 文章目录 day06-认识 CSS(三大特性、引入方式)作业…