重构安全模块,移除oauth2.0,使用springsecurity代替

This commit is contained in:
阿沐 2022-08-27 23:59:02 +08:00
parent 35da029ee4
commit 6c5e9884ba
28 changed files with 480 additions and 910 deletions

View File

@ -0,0 +1,41 @@
package net.maku.security.event;
import lombok.AllArgsConstructor;
import net.maku.framework.common.constant.Constant;
import net.maku.framework.security.user.UserDetail;
import net.maku.system.enums.LoginOperationEnum;
import net.maku.system.service.SysLogLoginService;
import org.springframework.context.event.EventListener;
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.stereotype.Component;
/**
* 认证事件处理
*
* @author 阿沐 babamu@126.com
*/
@Component
@AllArgsConstructor
public class AuthenticationEvents {
private final SysLogLoginService sysLogLoginService;
@EventListener
public void onSuccess(AuthenticationSuccessEvent event) {
// 用户信息
UserDetail user = (UserDetail) event.getAuthentication().getPrincipal();
// 保存登录日志
sysLogLoginService.save(user.getUsername(), Constant.SUCCESS, LoginOperationEnum.LOGIN_SUCCESS.getValue());
}
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent event) {
// 用户名
String username = (String) event.getAuthentication().getPrincipal();
// 保存登录日志
sysLogLoginService.save(username, Constant.FAIL, LoginOperationEnum.ACCOUNT_FAIL.getValue());
}
}

View File

@ -1,58 +0,0 @@
package net.maku.security.filter;
import lombok.AllArgsConstructor;
import net.maku.framework.security.exception.FastAuthenticationException;
import net.maku.framework.security.handler.UserAuthenticationFailureHandler;
import net.maku.security.service.CaptchaService;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
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;
/**
* 验证码过滤器
*
* @author 阿沐 babamu@126.com
*/
@Component
@AllArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {
private final static String OAUTH_TOKEN_URL = "/sys/oauth/token";
private final CaptchaService captchaService;
private final UserAuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if(request.getServletPath().equals(OAUTH_TOKEN_URL)
&& request.getMethod().equalsIgnoreCase("POST")
&& "password".equalsIgnoreCase(request.getParameter("grant_type"))) {
try {
// 校验验证码
validate(request);
}catch (AuthenticationException e) {
// 失败处理器
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
private void validate(HttpServletRequest request) {
String key = request.getParameter("key");
String captcha = request.getParameter("captcha");
boolean flag = captchaService.validate(key, captcha);
if(!flag) {
throw new FastAuthenticationException("验证码错误");
}
}
}

View File

@ -1,72 +0,0 @@
package net.maku.security.service;
import cn.hutool.core.util.StrUtil;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import lombok.AllArgsConstructor;
import net.maku.framework.common.utils.RedisKeys;
import net.maku.framework.common.utils.RedisUtils;
import org.springframework.stereotype.Service;
/**
* 验证码
*
* @author 阿沐 babamu@126.com
*/
@Service
@AllArgsConstructor
public class CaptchaService {
private final RedisUtils redisUtils;
/**
* 生成验证码
* @param key key
* @return 返回base64图片验证码
*/
public String generate(String key) {
// 生成验证码
SpecCaptcha captcha = new SpecCaptcha(150, 40);
captcha.setLen(5);
captcha.setCharType(Captcha.TYPE_DEFAULT);
// 保存到缓存
key = RedisKeys.getCaptchaKey(key);
redisUtils.set(key, captcha.text(), 300);
return captcha.toBase64();
}
/**
* 验证码效验
* @param key key
* @param code 验证码
* @return true成功 false失败
*/
public boolean validate(String key, String code) {
if(StrUtil.isBlank(key) || StrUtil.isBlank(code)){
return false;
}
// 获取验证码
String captcha = getCache(key);
// 效验成功
if(code.equalsIgnoreCase(captcha)){
return true;
}
return false;
}
private String getCache(String key){
key = RedisKeys.getCaptchaKey(key);
String captcha = (String)redisUtils.get(key);
// 删除验证码
if(captcha != null){
redisUtils.delete(key);
}
return captcha;
}
}

View File

@ -1,82 +0,0 @@
package net.maku.security.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import net.maku.framework.common.utils.JsonUtils;
import net.maku.system.dao.SysOauthClientDao;
import net.maku.system.entity.SysOauthClientEntity;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Map;
/**
* ClientDetailsService
*
* @author 阿沐 babamu@126.com
*/
@Service
@AllArgsConstructor
public class FastClientDetailsService implements ClientDetailsService {
private final SysOauthClientDao sysOauthClientDao;
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
SysOauthClientEntity oauthClient = sysOauthClientDao.getByClientId(clientId);
return clientDetailsMapper(oauthClient);
}
private ClientDetails clientDetailsMapper(SysOauthClientEntity entity) {
BaseClientDetails client = new BaseClientDetails();
client.setClientId(entity.getClientId());
// 密码前追加 {noop} 前缀表示密码是明文
client.setClientSecret(String.format("{noop}%s", entity.getClientSecret()));
if (ArrayUtil.isNotEmpty(entity.getAuthorizedGrantTypes())) {
client.setAuthorizedGrantTypes(CollUtil.newArrayList(entity.getAuthorizedGrantTypes()));
}
if (StrUtil.isNotBlank(entity.getAuthorities())) {
client.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(entity.getAuthorities()));
}
if (StrUtil.isNotBlank(entity.getResourceIds())) {
client.setResourceIds(StringUtils.commaDelimitedListToSet(entity.getResourceIds()));
}
if (StrUtil.isNotBlank(entity.getWebServerRedirectUri())) {
client.setRegisteredRedirectUri(StringUtils.commaDelimitedListToSet(entity.getWebServerRedirectUri()));
}
if (StrUtil.isNotBlank(entity.getScope())) {
client.setScope(StringUtils.commaDelimitedListToSet(entity.getScope()));
}
if (StrUtil.isNotBlank(entity.getAutoapprove())) {
client.setAutoApproveScopes(StringUtils.commaDelimitedListToSet(entity.getAutoapprove()));
}
if (entity.getAccessTokenValidity() != null) {
client.setAccessTokenValiditySeconds(entity.getAccessTokenValidity());
}
if (entity.getRefreshTokenValidity() != null) {
client.setRefreshTokenValiditySeconds(entity.getRefreshTokenValidity());
}
if (StrUtil.isNotBlank(entity.getAdditionalInformation())) {
Map<String, Object> map = JsonUtils.parseObject(entity.getAdditionalInformation(), Map.class);
client.setAdditionalInformation(map);
}
return client;
}
}

View File

@ -1,8 +1,6 @@
package net.maku.security.service;
import lombok.AllArgsConstructor;
import net.maku.framework.common.exception.ErrorCode;
import net.maku.framework.common.exception.FastException;
import net.maku.framework.security.user.UserDetail;
import net.maku.system.convert.SysUserConvert;
import net.maku.system.dao.SysRoleDao;
@ -13,18 +11,14 @@ import net.maku.system.enums.DataScopeEnum;
import net.maku.system.enums.UserStatusEnum;
import net.maku.system.service.SysMenuService;
import net.maku.system.service.SysOrgService;
import org.springframework.security.core.GrantedAuthority;
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.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* UserDetailsService
@ -43,15 +37,15 @@ public class FastUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUserEntity userEntity = sysUserDao.getByUsername(username);
if(userEntity == null) {
throw new FastException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
if (userEntity == null) {
throw new UsernameNotFoundException("用户名或密码错误");
}
// 转换成UserDetail对象
UserDetail userDetail = SysUserConvert.INSTANCE.convertDetail(userEntity);
// 账号不可用
if(userEntity.getStatus() == UserStatusEnum.DISABLE.getValue()){
if (userEntity.getStatus() == UserStatusEnum.DISABLE.getValue()) {
userDetail.setEnabled(false);
}
@ -60,15 +54,15 @@ public class FastUserDetailsService implements UserDetailsService {
userDetail.setDataScopeList(dataScopeList);
// 用户权限列表
Set<GrantedAuthority> authorities = getUserAuthority(userDetail);
userDetail.setAuthorities(authorities);
Set<String> authoritySet = sysMenuService.getUserAuthority(userDetail);
userDetail.setAuthoritySet(authoritySet);
return userDetail;
}
private List<Long> getDataScope(UserDetail userDetail){
private List<Long> getDataScope(UserDetail userDetail) {
Integer dataScope = sysRoleDao.getDataScopeByUserId(userDetail.getId());
if (dataScope == null){
if (dataScope == null) {
return new ArrayList<>();
}
@ -98,14 +92,4 @@ public class FastUserDetailsService implements UserDetailsService {
return new ArrayList<>();
}
private Set<GrantedAuthority> getUserAuthority(UserDetail user) {
// 获取用户权限标识
Set<String> permsSet = sysMenuService.getUserAuthority(user);
// 封装权限标识
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.addAll(permsSet.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
return authorities;
}
}

View File

@ -1,32 +0,0 @@
package net.maku.security.service;
import lombok.AllArgsConstructor;
import net.maku.framework.common.utils.RedisKeys;
import net.maku.framework.common.utils.RedisUtils;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
import org.springframework.stereotype.Service;
/**
* 基于Redis的授权码模式
*
* @author 阿沐 babamu@126.com
*/
@Service
@AllArgsConstructor
public class RedisAuthorizationCodeServices extends RandomValueAuthorizationCodeServices {
private final RedisUtils redisUtils;
@Override
protected void store(String code, OAuth2Authentication authentication) {
String key = RedisKeys.getOauthCode(code);
redisUtils.set(key, authentication);
}
@Override
public OAuth2Authentication remove(String code) {
String key = RedisKeys.getOauthCode(code);
return (OAuth2Authentication)redisUtils.get(key);
}
}

View File

@ -1,12 +1,5 @@
auth:
ignore_urls:
- /actuator/**
- /v3/api-docs/**
- /webjars/**
- /swagger/**
- /swagger-resources/**
- /swagger-ui.html
- /swagger-ui/**
- /doc.html
- /sys/oauth/captcha
- /sys/auth/captcha
- /sys/auth/login
- /upload/**

View File

@ -1,73 +1,60 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>net.maku</groupId>
<artifactId>fast-boot</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fast-framework</artifactId>
<packaging>jar</packaging>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>net.maku</groupId>
<artifactId>fast-boot</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fast-framework</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<exclusions>
<exclusion>
<artifactId>kotlin-stdlib-jdk7</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
</exclusion>
<exclusion>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,4 +1,4 @@
package net.maku.framework.common.utils;
package net.maku.framework.common.cache;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
@ -10,38 +10,46 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类
* Redis Cache
*
* @author 阿沐 babamu@126.com
*/
@Component
public class RedisUtils {
public class RedisCache {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/** 默认过期时长为24小时单位秒 */
/**
* 默认过期时长为24小时单位
*/
public final static long DEFAULT_EXPIRE = 60 * 60 * 24L;
/** 过期时长为1小时单位秒 */
/**
* 过期时长为1小时单位
*/
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
/** 过期时长为6小时单位秒 */
/**
* 过期时长为6小时单位
*/
public final static long HOUR_SIX_EXPIRE = 60 * 60 * 6L;
/** 不设置过期时长 */
/**
* 不设置过期时长
*/
public final static long NOT_EXPIRE = -1L;
public void set(String key, Object value, long expire){
public void set(String key, Object value, long expire) {
redisTemplate.opsForValue().set(key, value);
if(expire != NOT_EXPIRE){
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
}
public void set(String key, Object value){
public void set(String key, Object value) {
set(key, value, DEFAULT_EXPIRE);
}
public Object get(String key, long expire) {
Object value = redisTemplate.opsForValue().get(key);
if(expire != NOT_EXPIRE){
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
return value;
@ -51,11 +59,11 @@ public class RedisUtils {
return get(key, NOT_EXPIRE);
}
public Long increment(String key){
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key);
}
public Boolean hasKey(String key){
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
@ -71,19 +79,19 @@ public class RedisUtils {
return redisTemplate.opsForHash().get(key, field);
}
public Map<String, Object> hGetAll(String key){
public Map<String, Object> hGetAll(String key) {
HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
return hashOperations.entries(key);
}
public void hMSet(String key, Map<String, Object> map){
public void hMSet(String key, Map<String, Object> map) {
hMSet(key, map, DEFAULT_EXPIRE);
}
public void hMSet(String key, Map<String, Object> map, long expire){
public void hMSet(String key, Map<String, Object> map, long expire) {
redisTemplate.opsForHash().putAll(key, map);
if(expire != NOT_EXPIRE){
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
}
@ -95,32 +103,32 @@ public class RedisUtils {
public void hSet(String key, String field, Object value, long expire) {
redisTemplate.opsForHash().put(key, field, value);
if(expire != NOT_EXPIRE){
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
}
public void expire(String key, long expire){
public void expire(String key, long expire) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
public void hDel(String key, Object... fields){
public void hDel(String key, Object... fields) {
redisTemplate.opsForHash().delete(key, fields);
}
public void leftPush(String key, Object value){
public void leftPush(String key, Object value) {
leftPush(key, value, DEFAULT_EXPIRE);
}
public void leftPush(String key, Object value, long expire){
public void leftPush(String key, Object value, long expire) {
redisTemplate.opsForList().leftPush(key, value);
if(expire != NOT_EXPIRE){
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
}
public Object rightPop(String key){
public Object rightPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
}

View File

@ -1,4 +1,4 @@
package net.maku.framework.common.utils;
package net.maku.framework.common.cache;
/**
* Redis Key管理
@ -15,10 +15,10 @@ public class RedisKeys {
}
/**
* 授权码Key
* accessToken Key
*/
public static String getOauthCode(String code) {
return "oauth:code:" + code;
public static String getAccessTokenKey(String accessToken) {
return "sys:access:" + accessToken;
}
}

View File

@ -0,0 +1,33 @@
package net.maku.framework.security.cache;
import lombok.AllArgsConstructor;
import net.maku.framework.common.cache.RedisCache;
import net.maku.framework.common.cache.RedisKeys;
import net.maku.framework.security.user.UserDetail;
import org.springframework.stereotype.Component;
/**
* 认证 Cache
*
* @author 阿沐 babamu@126.com
*/
@Component
@AllArgsConstructor
public class TokenStoreCache {
private final RedisCache redisCache;
public void saveUser(String accessToken, UserDetail user) {
String key = RedisKeys.getAccessTokenKey(accessToken);
redisCache.set(key, user);
}
public UserDetail getUser(String accessToken) {
String key = RedisKeys.getAccessTokenKey(accessToken);
return (UserDetail) redisCache.get(key);
}
public void deleteUser(String accessToken) {
String key = RedisKeys.getAccessTokenKey(accessToken);
redisCache.delete(key);
}
}

View File

@ -1,76 +0,0 @@
package net.maku.framework.security.config;
import lombok.AllArgsConstructor;
import net.maku.framework.security.exception.FastWebResponseExceptionTranslator;
import net.maku.framework.security.token.FastTokenEnhancer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
/**
* 认证服务器配置
*
* @author 阿沐 babamu@126.com
*/
@Configuration
@AllArgsConstructor
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
private final ClientDetailsService fastClientDetailsService;
private final UserDetailsService userDetailsService;
private final AuthorizationCodeServices redisAuthorizationCodeServices;
private final TokenStore tokenStore;
/**
* 配置客户端信息
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(fastClientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE);
// 密码模式
endpoints.authenticationManager(authenticationManager);
// 支持刷新令牌
endpoints.userDetailsService(userDetailsService);
// 令牌管理
endpoints.tokenStore(tokenStore);
// 令牌增强
endpoints.tokenEnhancer(tokenEnhancer());
// 登录或者鉴权失败时的返回信息
endpoints.exceptionTranslator(new FastWebResponseExceptionTranslator());
// 配置授权码模式存放在Redis中
endpoints.authorizationCodeServices(redisAuthorizationCodeServices);
// 自定义登录地址
endpoints.pathMapping("/oauth/token","/sys/oauth/token");
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new FastTokenEnhancer();
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()") // 匿名可访问/oauth/token_key
.checkTokenAccess("isAuthenticated()") // 认证后可访问/oauth/check_token
;
}
}

View File

@ -21,10 +21,10 @@ import java.util.Properties;
@Component
public class PermitResource {
/**
* 指定被 spring security oauth2.0 忽略的URL
* 指定被 spring security 忽略的URL
*/
@SneakyThrows
public List<String> getPermitList(){
public List<String> getPermitList() {
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:auth.yml");
String key = "auth.ignore_urls";
@ -32,16 +32,16 @@ public class PermitResource {
return getPropertiesList(key, resources);
}
private List<String> getPropertiesList(String key, Resource... resources){
private List<String> getPropertiesList(String key, Resource... resources) {
List<String> list = new ArrayList<>();
// 解析资源文件
for(Resource resource : resources) {
for (Resource resource : resources) {
Properties properties = loadYamlProperties(resource);
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String tmpKey = StringUtils.substringBefore(entry.getKey().toString(), "[");
if(tmpKey.equalsIgnoreCase(key)){
if (tmpKey.equalsIgnoreCase(key)) {
list.add(entry.getValue().toString());
}
}

View File

@ -1,50 +0,0 @@
package net.maku.framework.security.config;
import lombok.AllArgsConstructor;
import net.maku.framework.security.exception.SecurityAuthenticationEntryPoint;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import java.util.List;
/**
* 资源服务器配置
*
* @author 阿沐 babamu@126.com
*/
@Configuration
@AllArgsConstructor
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private final TokenStore tokenStore;
private final PermitResource permitResource;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore);
resources.authenticationEntryPoint(new SecurityAuthenticationEntryPoint());
}
@Override
public void configure(HttpSecurity http) throws Exception {
// 忽略授权的地址列表
List<String> permitList = permitResource.getPermitList();
String [] permits = permitList.toArray(new String[permitList.size()]);
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(permits).permitAll()
.anyRequest().authenticated()
;
}
}

View File

@ -0,0 +1,69 @@
package net.maku.framework.security.config;
import lombok.AllArgsConstructor;
import net.maku.framework.security.exception.SecurityAuthenticationEntryPoint;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import java.util.List;
@Configuration
@AllArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final OncePerRequestFilter authenticationTokenFilter;
private final PermitResource permitResource;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 忽略授权的地址列表
List<String> permitList = permitResource.getPermitList();
String[] permits = permitList.toArray(new String[0]);
http
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers(permits).permitAll()
.anyRequest().authenticated()
.and().userDetailsService(userDetailsService)
.exceptionHandling().authenticationEntryPoint(new SecurityAuthenticationEntryPoint())
.and().headers().frameOptions().disable()
.and().csrf().disable()
;
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().antMatchers(HttpMethod.OPTIONS);
}
}

View File

@ -1,25 +0,0 @@
package net.maku.framework.security.config;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
/**
* TokenStore
*
* @author 阿沐 babamu@126.com
*/
@Configuration
@AllArgsConstructor
public class TokenStoreConfig {
private final RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore tokenStore() {
// 使用redis存储token
return new RedisTokenStore(redisConnectionFactory);
}
}

View File

@ -1,61 +0,0 @@
package net.maku.framework.security.config;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Spring Security配置
*
* @author 阿沐 babamu@126.com
*/
@AllArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final OncePerRequestFilter validateCodeFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 密码模式需要
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManager();
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.and()
.authorizeRequests()
.antMatchers("/oauth/authorize").authenticated()
.anyRequest().permitAll()
.and().headers().frameOptions().disable()
.and().csrf().disable()
;
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(HttpMethod.OPTIONS);
}
}

View File

@ -1,18 +0,0 @@
package net.maku.framework.security.exception;
import org.springframework.security.core.AuthenticationException;
/**
* 认证异常类
*
* @author 阿沐 babamu@126.com
*/
public class FastAuthenticationException extends AuthenticationException {
public FastAuthenticationException(String msg, Throwable t) {
super(msg, t);
}
public FastAuthenticationException(String msg) {
super(msg);
}
}

View File

@ -1,31 +0,0 @@
package net.maku.framework.security.exception;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
/**
* 自定义异常
*
* @author 阿沐 babamu@126.com
*/
public class FastOAuth2Exception extends OAuth2Exception {
private String msg;
public FastOAuth2Exception(String msg) {
super(msg);
this.msg = msg;
}
public FastOAuth2Exception(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

View File

@ -1,78 +0,0 @@
package net.maku.framework.security.exception;
import net.maku.framework.common.exception.ErrorCode;
import net.maku.framework.common.utils.Result;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.ClientAuthenticationException;
import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.stereotype.Component;
import org.springframework.web.HttpRequestMethodNotSupportedException;
/**
* 登录或者鉴权失败时的返回信息
*
* @author 阿沐 babamu@126.com
*/
@Component
public class FastWebResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> {
private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) {
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
Exception exception = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (exception != null) {
return handleOAuth2Exception(new FastOAuth2Exception(e.getMessage(), e));
}
exception = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
if (exception != null) {
return handleOAuth2Exception(new FastOAuth2Exception(exception.getMessage(), exception));
}
exception = (InvalidGrantException) throwableAnalyzer.getFirstThrowableOfType(InvalidGrantException.class, causeChain);
if (exception != null) {
return handleOAuth2Exception(new FastOAuth2Exception(exception.getMessage(), exception));
}
exception = (HttpRequestMethodNotSupportedException) throwableAnalyzer.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain);
if (exception != null) {
return handleOAuth2Exception(new FastOAuth2Exception(exception.getMessage(), exception));
}
exception = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
if (exception != null) {
return handleOAuth2Exception((OAuth2Exception) exception);
}
return handleOAuth2Exception(new FastOAuth2Exception(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
}
private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) {
int status = e.getHttpErrorCode();
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CACHE_CONTROL, "no-store");
headers.set(HttpHeaders.PRAGMA, "no-cache");
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set(HttpHeaders.WWW_AUTHENTICATE, String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
if (e instanceof ClientAuthenticationException) {
return new ResponseEntity<>(e, headers, HttpStatus.valueOf(status));
}
Result<String> result = Result.error(ErrorCode.ACCOUNT_PASSWORD_ERROR);
return new ResponseEntity(result, headers, HttpStatus.OK);
}
}

View File

@ -0,0 +1,57 @@
package net.maku.framework.security.filter;
import lombok.AllArgsConstructor;
import net.maku.framework.security.cache.TokenStoreCache;
import net.maku.framework.security.user.UserDetail;
import net.maku.framework.security.utils.TokenUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
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;
/**
* 认证过滤器
*
* @author 阿沐 babamu@126.com
*/
@Component
@AllArgsConstructor
public class AuthenticationTokenFilter extends OncePerRequestFilter {
private final TokenStoreCache tokenStoreCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String accessToken = TokenUtils.getAccessToken(request);
// accessToken为空表示未登录
if (StringUtils.isBlank(accessToken)) {
chain.doFilter(request, response);
return;
}
// 获取登录用户信息
UserDetail user = tokenStoreCache.getUser(accessToken);
if (user == null) {
chain.doFilter(request, response);
return;
}
// 用户存在
Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
// 新建 SecurityContext
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
chain.doFilter(request, response);
}
}

View File

@ -1,31 +0,0 @@
package net.maku.framework.security.handler;
import lombok.SneakyThrows;
import net.maku.framework.common.utils.HttpContextUtils;
import net.maku.framework.common.utils.JsonUtils;
import net.maku.framework.common.utils.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 认证失败处理器
*
* @author 阿沐 babamu@126.com
*/
@Component
public class UserAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@SneakyThrows
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
response.getWriter().write(JsonUtils.toJsonString(Result.error(e.getMessage())));
}
}

View File

@ -1,34 +0,0 @@
package net.maku.framework.security.token;
import net.maku.framework.common.utils.Result;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import java.util.HashMap;
import java.util.Map;
/**
* 令牌
*
* @author 阿沐 babamu@126.com
*/
public class FastTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
if(accessToken instanceof DefaultOAuth2AccessToken){
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
// 增加额外信息
Map<String, Object> info = new HashMap<>();
info.put("code", new Result<>().getCode());
token.setAdditionalInformation(info);
return token;
}
return accessToken;
}
}

View File

@ -1,13 +1,15 @@
package net.maku.framework.security.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
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.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 登录用户信息
@ -29,10 +31,10 @@ public class UserDetail implements UserDetails {
private Long orgId;
private Integer status;
private Integer superAdmin;
private Date createTime;
/**
* 数据权限范围
*
* <p>
* null表示全部数据权限
*/
private List<Long> dataScopeList;
@ -55,11 +57,12 @@ public class UserDetail implements UserDetails {
/**
* 拥有权限集合
*/
private Set<GrantedAuthority> authorities;
private Set<String> authoritySet;
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
return authoritySet.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
}
@Override

View File

@ -0,0 +1,33 @@
package net.maku.framework.security.utils;
import cn.hutool.core.lang.UUID;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* Token 工具类
*
* @author 阿沐 babamu@126.com
*/
public class TokenUtils {
/**
* 生成 AccessToken
*/
public static String generator() {
return UUID.fastUUID().toString(true);
}
/**
* 获取 AccessToken
*/
public static String getAccessToken(HttpServletRequest request) {
String accessToken = request.getHeader("Authorization");
if (StringUtils.isBlank(accessToken)) {
accessToken = request.getParameter("access_token");
}
return accessToken;
}
}

View File

@ -0,0 +1,10 @@
auth:
ignore_urls:
- /actuator/**
- /v3/api-docs/**
- /webjars/**
- /swagger/**
- /swagger-resources/**
- /swagger-ui.html
- /swagger-ui/**
- /doc.html