文章目录
- SpringSecurity介绍
- sss-security实现
- 依赖
- 工具类
- Jwt工具
- JSON响应工具
- 加密工具类
- 用户上下文
- 用户信息实体类
- 用户上下文
- 自定义重写
- 自定义无权限的报错
- 自定义密码加密
- 自定义用户类
- 过滤器
- 登录过滤器
- 权限过滤器
- Service
- 登录Service
- 配置类
- 说明
- 登录验证
- 权限验证
- IP流量限制
- sss-system模块实现
- Service实现
- 登录日志实现类
- UserDetailsService实现类
- 测试
- 登录失败测试
- 登录成功测试
- 其他请求测试
- 其他建议
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;
}
// 使用提供的密钥解析并验证JWT
Jws<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) {
// 记录解析过程中的任何异常,并返回null
e.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()构建JWT
String 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属性并返回;否则返回null
return 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 {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) {
// 获取请求的URI
String 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
*/
@Override
public String encode(CharSequence rawPassword) {
return EncryptionUtil.saltMd5Encrypt(rawPassword.toString());
}
/**
* 判断用户所输入的密码和加密之后的密码是否相同
*
* @param rawPassword
* @param encodedPassword
* @return
*/
@Override
public 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;
// 不只是可以post
this.setPostOnly(false);
// 指定登录接口及提交方式,可以指定任意路径(我们默认的登陆路径)
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/system/login/login", "POST"));
}
/**
* 登录认证,覆盖父类实现
*
* @param req HTTP请求对象
* @param res HTTP响应对象
* @return 认证后的Authentication对象
* @throws AuthenticationException 认证过程中抛出的异常
*/
@Override
public 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
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) {
System.out.println("登录成功,生成token------------------------------------------------------------------------------------------");
// 获取当前用户信息
CustomUser customUser = (CustomUser) auth.getPrincipal();
// 保存权限数据到redis
String redisKey = RedisConstant.AUTHORITY_PERMISSION + customUser.getUsername();
System.out.println("保存用户权限到redis中,redisKey:" + redisKey);
//设置缓存过期时间是十五天
redisTemplate.opsForValue().set(redisKey,
JSON.toJSONString(customUser.getAuthorities()),
15,
TimeUnit.DAYS);
// 生成token
UserEntity 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 认证失败异常
*/
@Override
protected 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
*/
@Override
protected 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或获取权限信息,返回null
return 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;
@Autowired
private CustomMd5PasswordEncoder customMd5PasswordEncoder;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
// @Qualifier("systemRecordLoginLogServiceImpl") // 指定实现类
private RecordLoginLogService loginLogService;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
private IpFlowControlConfiguration ipFlowControlConfiguration;
/**
* 创建并返回一个AuthenticationManager实例,此方法由父类WebSecurityConfigurerAdapter提供
*
* @return
* @throws Exception
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected 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);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定 UserDetailService 和 加密器
auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);
}
/**
* 配置哪些请求不拦截
* 重写configure(WebSecurity)方法
* 使用web.ignoring().antMatchers(...)指定一系列接口路径,
* 这些路径的请求将不会经过Spring Security的过滤链,即不受安全约束。
*
* @param web
* @throws Exception
*/
@Override
public 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 {
@Autowired
private LoginLogDao loginLogDao;
@Override
public 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;
// }
@Override
public 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 {
@Autowired
private UserService sysUserService;
@Autowired
private MenuService menuService;
@Override
public 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 {
@Autowired
private UserService sysUserService;
@Autowired
private MenuService menuService;
@Override
public 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
其他建议
如果对用户权限标识存储有什么不理解的地方,可以参考【智能排班系统】数据库设计的菜单表
、角色表
、用户表
、角色菜单中间表
、用户角色中间表
。