This commit is contained in:
2023-05-16 17:28:43 +08:00
parent 77a83cee8b
commit a161a83023
64 changed files with 1367 additions and 212 deletions

View File

@ -0,0 +1,96 @@
package com.qiaoba.auth.config;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.SecureUtil;
import com.qiaoba.auth.constants.SecurityConstant;
import com.qiaoba.auth.filters.JwtAuthenticationTokenFilter;
import com.qiaoba.auth.handler.AccessDeniedHandler;
import com.qiaoba.auth.handler.LogoutHandler;
import com.qiaoba.auth.properties.AuthConfigProperties;
import com.qiaoba.auth.utils.TokenUtil;
import com.qiaoba.common.redis.service.RedisService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.PostConstruct;
/**
* SpringSecurity安全配置
*
* @author ailanyin
* @version 1.0
* @since 2023/5/15 11:23
*/
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class SpringSecurityConfig {
private final AuthConfigProperties authConfigProperties;
private final AccessDeniedHandler accessDeniedHandler;
private final LogoutHandler logoutHandler;
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private final RedisService redisService;
/**
* 创建Token秘钥
*/
@PostConstruct
public void initSecret() {
if (redisService.hasKey(SecurityConstant.REDIS_SECRET_KEY)) {
TokenUtil.secret = SecureUtil.md5(SecureUtil.md5(redisService.get(SecurityConstant.REDIS_SECRET_KEY).toString()));
} else {
String random = RandomUtil.randomString(8);
TokenUtil.secret = SecureUtil.md5(SecureUtil.md5(random));
redisService.set(SecurityConstant.REDIS_SECRET_KEY, random);
}
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
//白名单
for (String url : authConfigProperties.getWhitelist()) {
registry.antMatchers(url).permitAll();
}
// 由于使用的是JWT我们这里不需要csrf
httpSecurity.csrf()
.disable()
//添加自定义未授权和未登录结果返回
.exceptionHandling()
.authenticationEntryPoint(accessDeniedHandler)
.and()
// 基于token所以不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//跨域请求会先进行一次options请求
.antMatchers(HttpMethod.OPTIONS)
.permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest()
.authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 退出处理
httpSecurity.logout().logoutUrl(SecurityConstant.LOGOUT_URL).logoutSuccessHandler(logoutHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}

View File

@ -0,0 +1,81 @@
package com.qiaoba.auth.constants;
import java.util.Arrays;
import java.util.List;
/**
* 安全常量
*
* @author ailanyin
* @version 1.0
* @since 2022/9/8 0008 下午 14:54
*/
public class SecurityConstant {
public static final int MAX_ERROR_COUNT = 5;
public static final String LOGOUT_URL = "/logout";
public static final String HAS_BEEN_PULLED_BLACK = "您的IP已经被系统拉黑";
public static final String ACCESS_DENIED = "暂无权限访问, 请重新登录";
public static final String BLACKLIST_KEY = "login:blacklist";
public static final String LOGIN_ERROR_COUNT = "login:errorCount:";
public static final String BLACKLIST_ON = "true";
public static final String BLACKLIST_ON_OFF_KEY = "sys_config:sys.account.blacklistOnOff";
public static final String CAPTCHA_KEY = "login:captcha:";
public static final String CAPTCHA_ON_OFF_KEY = "sys_config:sys.account.captchaOnOff";
public static final String CAPTCHA_ON = "true";
public static final String REGISTER_ON_OFF_KEY = "sys_config:sys.account.registerUser";
public static final String REGISTER_ON = "true";
public static final String REDIS_SECRET_KEY = "sys:secret:secret";
/**
* 登录成功
*/
public static final String LOGIN_SUCCESS = "登录成功";
/**
* 登录失败
*/
public static final String LOGIN_FAIL = "登录失败";
/**
* 密码错误
*/
public static final String PASSWORD_ERROR = "密码错误";
/**
* token header
*/
public static final String TOKEN_HEADER = "Authorization";
/**
* token前缀
*/
public static final String TOKEN_HEAD = "Bearer ";
/**
* Xss过滤白名单
*/
public final static List<String> XSS_WHITELIST = Arrays.asList(
"/captchaImage" ,
"/login",
"/workflow/process/start",
"/workflow/model/save"
);
/**
* 需要限流的接口
*/
public final static List<String> LIMIT_URI = Arrays.asList(
"/captchaImage" ,
"/login" ,
"/register"
);
/**
* 限流的RedisKey
*/
public final static String RATE_LIMIT_KEY = "rateLimit:";
/**
* 同IP每秒最大允许访问次数
*/
public final static Integer MAX_RATE_LIMIT_TOTAL = 5;
}

View File

@ -0,0 +1,172 @@
package com.qiaoba.auth.entity;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 登录用户身份权限
*
* @author ailanyin
* @version 1.0
* @since 2021/10/15 0015 上午 10:05
*/
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private String userId;
/**
* 部门ID
*/
private String deptId;
/**
* 登录账号
*/
private String username;
/**
* 用户名称
*/
private String nickname;
/**
* 角色列表
*/
private List<String> roleIds;
/**
* 权限列表
*/
private Set<String> permissions;
public LoginUser() {
}
public LoginUser(String userId, String deptId, String username, String nickname, List<String> roleIds, Set<String> permissions) {
this.userId = userId;
this.deptId = deptId;
this.username = username;
this.permissions = permissions;
this.nickname = nickname;
this.roleIds = roleIds;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getDeptId() {
return deptId;
}
public void setDeptId(String deptId) {
this.deptId = deptId;
}
public List<String> getRoleIds() {
return roleIds;
}
public void setRoleIds(List<String> roleIds) {
this.roleIds = roleIds;
}
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public String getPassword() {
return null;
}
/**
* 账户是否未过期,过期无法验证
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*
* @return boolean
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return boolean
*/
@Override
public boolean isEnabled() {
return true;
}
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
//返回当前用户的权限
return permissions.stream()
.filter(StrUtil::isNotBlank)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,62 @@
package com.qiaoba.auth.filters;
import cn.hutool.core.util.StrUtil;
import com.qiaoba.auth.constants.SecurityConstant;
import com.qiaoba.auth.properties.AuthConfigProperties;
import com.qiaoba.auth.utils.TokenUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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.Objects;
/**
* JwtAuthenticationTokenFilter
* 为了保证 SecurityContext 上下文中 userInfo 是最新的
*
* @author ailanyin
* @version 1.0
* @since 2021/10/21 0021 下午 14:13
*/
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final AuthConfigProperties authConfigProperties;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 鉴权白名单, 放行
if (authConfigProperties.getWhitelist().contains(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
// 取Header中的Token
String authHeader = request.getHeader(SecurityConstant.TOKEN_HEADER);
if (StrUtil.isNotBlank(authHeader) && authHeader.startsWith(SecurityConstant.TOKEN_HEAD)) {
String authToken = authHeader.substring(SecurityConstant.TOKEN_HEAD.length());
if (TokenUtil.validateToken(authToken)) {
String username = TokenUtil.getUserNameFromToken(authToken);
if (StrUtil.isNotBlank(username) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}

View File

@ -0,0 +1,31 @@
package com.qiaoba.auth.handler;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONUtil;
import com.qiaoba.auth.constants.SecurityConstant;
import com.qiaoba.common.base.result.AjaxResult;
import com.qiaoba.common.web.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 无权限处理器
*
* @author ailanyin
* @version 1.0
* @since 2023/5/15 14:14
*/
@Component
public class AccessDeniedHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
AjaxResult result = AjaxResult.error(HttpStatus.HTTP_UNAUTHORIZED, SecurityConstant.ACCESS_DENIED);
ResponseUtil.response(response, JSONUtil.toJsonStr(result));
}
}

View File

@ -0,0 +1,26 @@
package com.qiaoba.auth.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 退出处理器
*
* @author ailanyin
* @version 1.0
* @since 2022/1/4 0004 下午 16:20
*/
@Component
public class LogoutHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse httpServletResponse, Authentication authentication) {
//删除SecurityContext上下文中的信息
SecurityContextHolder.clearContext();
}
}

View File

@ -0,0 +1,24 @@
package com.qiaoba.auth.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* 鉴权配置(白名单/XSS)
*
* @author ailanyin
* @version 1.0
* @since 2023/5/15 11:09
*/
@Configuration
@ConfigurationProperties(prefix = "qiaoba.auth")
@Data
@EnableConfigurationProperties
public class AuthConfigProperties {
private List<String> whitelist;
}

View File

@ -0,0 +1,74 @@
package com.qiaoba.auth.utils;
import com.qiaoba.auth.entity.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* SecurityUtil
*
* @author ailanyin
* @version 1.0
* @since 2022/8/16 0016 下午 14:51
*/
public class SecurityUtil {
private static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
/**
* 获取登录用户的账号
*
* @return username
*/
public static String getLoginUsername() {
return getLoginUser().getUsername();
}
/**
* 获取登录用户的账号
*
* @return nickname
*/
public static String getLoginNickname() {
return getLoginUser().getNickname();
}
/**
* 获取登录用户的ID
*
* @return userId
*/
public static String getLoginUserId() {
return getLoginUser().getUserId();
}
public static LoginUser getLoginUser() {
SecurityContext ctx = SecurityContextHolder.getContext();
Authentication auth = ctx.getAuthentication();
return (LoginUser) auth.getPrincipal();
}
/**
* 加密密码
*
* @param password 原密码
* @return 加密后密码
*/
public static String encryptPassword(String password) {
return PASSWORD_ENCODER.encode(password);
}
/**
* 判断密码是否相同
*
* @param rawPassword 真实密码
* @param encodedPassword 加密后字符
* @return 结果
*/
public static boolean matchesPassword(String rawPassword, String encodedPassword) {
return PASSWORD_ENCODER.matches(rawPassword, encodedPassword);
}
}

View File

@ -0,0 +1,70 @@
package com.qiaoba.auth.utils;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import java.util.HashMap;
import java.util.Map;
/**
* TokenUtil
*
* @author ailanyin
* @version 1.0
* @since 2022/6/8 0008 上午 11:44
*/
public class TokenUtil {
/**
* jwt 加解密密钥,第一次项目启动时创建随机数
*/
public static String secret;
public static String generateToken(String username) {
DateTime now = DateTime.now();
// 3天过期
DateTime newTime = now.offsetNew(DateField.HOUR, 72);
Map<String, Object> payload = new HashMap<String, Object>(4);
//签发时间
payload.put(JWTPayload.ISSUED_AT, now);
//过期时间
payload.put(JWTPayload.EXPIRES_AT, newTime);
//生效时间
payload.put(JWTPayload.NOT_BEFORE, now);
//载荷
payload.put(JWTPayload.SUBJECT, username);
return JWTUtil.createToken(payload, secret.getBytes());
}
public static String getUserNameFromToken(String token) {
try {
return JWTUtil.parseToken(token).getPayload(JWTPayload.SUBJECT).toString();
} catch (Exception e) {
return null;
}
}
/**
* 验证Token是否有效
*
* @param token token
* @return 是/否
*/
public static boolean validateToken(String token) {
try {
if (!JWTUtil.verify(token, secret.getBytes())) {
return false;
}
long expireTime = Long.parseLong(JWTUtil.parseToken(token).getPayload(JWTPayload.EXPIRES_AT).toString() + "000");
return new DateTime(expireTime).after(DateTime.now());
} catch (Exception e) {
return false;
}
}
}