lym 1 vecka sedan
förälder
incheckning
dfbefb56eb
17 ändrade filer med 1231 tillägg och 0 borttagningar
  1. 39 0
      forest-security/pom.xml
  2. 12 0
      forest-security/src/main/java/com/hwrj/cloud/security/annotation/CacheException.java
  3. 50 0
      forest-security/src/main/java/com/hwrj/cloud/security/aspect/RedisCacheAspect.java
  4. 51 0
      forest-security/src/main/java/com/hwrj/cloud/security/component/DynamicAccessDecisionManager.java
  5. 77 0
      forest-security/src/main/java/com/hwrj/cloud/security/component/DynamicSecurityFilter.java
  6. 64 0
      forest-security/src/main/java/com/hwrj/cloud/security/component/DynamicSecurityMetadataSource.java
  7. 16 0
      forest-security/src/main/java/com/hwrj/cloud/security/component/DynamicSecurityService.java
  8. 57 0
      forest-security/src/main/java/com/hwrj/cloud/security/component/JwtAuthenticationTokenFilter.java
  9. 27 0
      forest-security/src/main/java/com/hwrj/cloud/security/component/RestAuthenticationEntryPoint.java
  10. 32 0
      forest-security/src/main/java/com/hwrj/cloud/security/component/RestfulAccessDeniedHandler.java
  11. 21 0
      forest-security/src/main/java/com/hwrj/cloud/security/config/IgnoreUrlsConfig.java
  12. 63 0
      forest-security/src/main/java/com/hwrj/cloud/security/config/RedisConfig.java
  13. 127 0
      forest-security/src/main/java/com/hwrj/cloud/security/config/SecurityConfig.java
  14. 182 0
      forest-security/src/main/java/com/hwrj/cloud/security/service/RedisService.java
  15. 199 0
      forest-security/src/main/java/com/hwrj/cloud/security/service/impl/RedisServiceImpl.java
  16. 170 0
      forest-security/src/main/java/com/hwrj/cloud/security/util/JwtTokenUtil.java
  17. 44 0
      forest-security/src/main/java/com/hwrj/cloud/security/util/SpringUtil.java

+ 39 - 0
forest-security/pom.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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>
+        <artifactId>forest-mall-cloud</artifactId>
+        <groupId>com.hwrj.cloud</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.hwrj.cloud.security</groupId>
+    <artifactId>forest-security</artifactId>
+    <packaging>jar</packaging>
+    <dependencies>
+        <dependency>
+            <artifactId>forest-common</artifactId>
+            <groupId>com.hwrj.cloud.common</groupId>
+            <version>${forest.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 12 - 0
forest-security/src/main/java/com/hwrj/cloud/security/annotation/CacheException.java

@@ -0,0 +1,12 @@
+package com.hwrj.cloud.security.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义注解,有该注解的缓存方法会抛出异常
+ */
+@Documented
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface CacheException {
+}

+ 50 - 0
forest-security/src/main/java/com/hwrj/cloud/security/aspect/RedisCacheAspect.java

@@ -0,0 +1,50 @@
+package com.hwrj.cloud.security.aspect;
+
+import com.hwrj.cloud.security.annotation.CacheException;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+/**
+ * Redis缓存切面,防止Redis宕机影响正常业务逻辑
+ * Created by macro on 2020/3/17.
+ */
+@Aspect
+@Component
+@Order(2)
+public class RedisCacheAspect {
+    private static Logger LOGGER = LoggerFactory.getLogger(RedisCacheAspect.class);
+
+    @Pointcut("execution(public * com.hwrj.cloud.portal.service.*CacheService.*(..)) || execution(public * com.hwrj.cloud.service.*CacheService.*(..))")
+    public void cacheAspect() {
+    }
+
+    @Around("cacheAspect()")
+    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+        Object result = null;
+        try {
+            result = joinPoint.proceed();
+        } catch (Throwable throwable) {
+            //有CacheException注解的方法需要抛出异常
+            if (method.isAnnotationPresent(CacheException.class)) {
+                throw throwable;
+            } else {
+                LOGGER.error(throwable.getMessage());
+            }
+        }
+        return result;
+    }
+
+}

+ 51 - 0
forest-security/src/main/java/com/hwrj/cloud/security/component/DynamicAccessDecisionManager.java

@@ -0,0 +1,51 @@
+package com.hwrj.cloud.security.component;
+
+import cn.hutool.core.collection.CollUtil;
+import org.springframework.security.access.AccessDecisionManager;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * 动态权限决策管理器,用于判断用户是否有访问权限
+ * Created by macro on 2020/2/7.
+ */
+public class DynamicAccessDecisionManager implements AccessDecisionManager {
+
+    @Override
+    public void decide(Authentication authentication, Object object,
+                       Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
+        // 当接口未被配置资源时直接放行
+        if (CollUtil.isEmpty(configAttributes)) {
+            return;
+        }
+        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
+        while (iterator.hasNext()) {
+            ConfigAttribute configAttribute = iterator.next();
+            //将访问所需资源或用户拥有资源进行比对
+            String needAuthority = configAttribute.getAttribute();
+            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
+                if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
+                    return;
+                }
+            }
+        }
+        throw new AccessDeniedException("抱歉,您没有访问权限");
+    }
+
+    @Override
+    public boolean supports(ConfigAttribute configAttribute) {
+        return true;
+    }
+
+    @Override
+    public boolean supports(Class<?> aClass) {
+        return true;
+    }
+
+}

+ 77 - 0
forest-security/src/main/java/com/hwrj/cloud/security/component/DynamicSecurityFilter.java

@@ -0,0 +1,77 @@
+package com.hwrj.cloud.security.component;
+
+import com.hwrj.cloud.security.config.IgnoreUrlsConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.access.SecurityMetadataSource;
+import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
+import org.springframework.security.access.intercept.InterceptorStatusToken;
+import org.springframework.security.web.FilterInvocation;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * 动态权限过滤器,用于实现基于路径的动态权限过滤
+ * Created by macro on 2020/2/7.
+ */
+public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {
+
+    @Autowired
+    private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
+    @Autowired
+    private IgnoreUrlsConfig ignoreUrlsConfig;
+
+    @Autowired
+    public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
+        super.setAccessDecisionManager(dynamicAccessDecisionManager);
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+        HttpServletRequest request = (HttpServletRequest) servletRequest;
+        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
+        //OPTIONS请求直接放行
+        if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
+            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
+            return;
+        }
+        //白名单请求直接放行
+        PathMatcher pathMatcher = new AntPathMatcher();
+        for (String path : ignoreUrlsConfig.getUrls()) {
+            if(pathMatcher.match(path,request.getRequestURI())){
+                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
+                return;
+            }
+        }
+        //此处会调用AccessDecisionManager中的decide方法进行鉴权操作
+        InterceptorStatusToken token = super.beforeInvocation(fi);
+        try {
+            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
+        } finally {
+            super.afterInvocation(token, null);
+        }
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    @Override
+    public Class<?> getSecureObjectClass() {
+        return FilterInvocation.class;
+    }
+
+    @Override
+    public SecurityMetadataSource obtainSecurityMetadataSource() {
+        return dynamicSecurityMetadataSource;
+    }
+
+}

+ 64 - 0
forest-security/src/main/java/com/hwrj/cloud/security/component/DynamicSecurityMetadataSource.java

@@ -0,0 +1,64 @@
+package com.hwrj.cloud.security.component;
+
+import cn.hutool.core.util.URLUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.web.FilterInvocation;
+import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+
+/**
+ * 动态权限数据源,用于获取动态权限规则
+ * Created by macro on 2020/2/7.
+ */
+public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
+
+    private static Map<String, ConfigAttribute> configAttributeMap = null;
+    @Autowired
+    private DynamicSecurityService dynamicSecurityService;
+
+    @PostConstruct
+    public void loadDataSource() {
+        configAttributeMap = dynamicSecurityService.loadDataSource();
+    }
+
+    public void clearDataSource() {
+        configAttributeMap.clear();
+        configAttributeMap = null;
+    }
+
+    @Override
+    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
+        if (configAttributeMap == null) this.loadDataSource();
+        List<ConfigAttribute>  configAttributes = new ArrayList<>();
+        //获取当前访问的路径
+        String url = ((FilterInvocation) o).getRequestUrl();
+        String path = URLUtil.getPath(url);
+        PathMatcher pathMatcher = new AntPathMatcher();
+        Iterator<String> iterator = configAttributeMap.keySet().iterator();
+        //获取访问该路径所需资源
+        while (iterator.hasNext()) {
+            String pattern = iterator.next();
+            if (pathMatcher.match(pattern, path)) {
+                configAttributes.add(configAttributeMap.get(pattern));
+            }
+        }
+        // 未设置操作请求权限,返回空集合
+        return configAttributes;
+    }
+
+    @Override
+    public Collection<ConfigAttribute> getAllConfigAttributes() {
+        return null;
+    }
+
+    @Override
+    public boolean supports(Class<?> aClass) {
+        return true;
+    }
+
+}

+ 16 - 0
forest-security/src/main/java/com/hwrj/cloud/security/component/DynamicSecurityService.java

@@ -0,0 +1,16 @@
+package com.hwrj.cloud.security.component;
+
+import org.springframework.security.access.ConfigAttribute;
+
+import java.util.Map;
+
+/**
+ * 动态权限相关业务类
+ * Created by macro on 2020/2/7.
+ */
+public interface DynamicSecurityService {
+    /**
+     * 加载资源ANT通配符和资源对应MAP
+     */
+    Map<String, ConfigAttribute> loadDataSource();
+}

+ 57 - 0
forest-security/src/main/java/com/hwrj/cloud/security/component/JwtAuthenticationTokenFilter.java

@@ -0,0 +1,57 @@
+package com.hwrj.cloud.security.component;
+
+import com.hwrj.cloud.security.util.JwtTokenUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+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;
+
+/**
+ * JWT登录授权过滤器
+ * Created by macro on 2018/4/26.
+ */
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
+    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
+    @Autowired
+    private UserDetailsService userDetailsService;
+    @Autowired
+    private JwtTokenUtil jwtTokenUtil;
+    @Value("${jwt.tokenHeader}")
+    private String tokenHeader;
+    @Value("${jwt.tokenHead}")
+    private String tokenHead;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request,
+                                    HttpServletResponse response,
+                                    FilterChain chain) throws ServletException, IOException {
+        String authHeader = request.getHeader(this.tokenHeader);
+        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
+            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
+            String username = jwtTokenUtil.getUserNameFromToken(authToken);
+            LOGGER.info("checking username:{}", username);
+            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
+                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
+                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
+                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+                    LOGGER.info("authenticated user:{}", username);
+                    SecurityContextHolder.getContext().setAuthentication(authentication);
+                }
+            }
+        }
+        chain.doFilter(request, response);
+    }
+}

+ 27 - 0
forest-security/src/main/java/com/hwrj/cloud/security/component/RestAuthenticationEntryPoint.java

@@ -0,0 +1,27 @@
+package com.hwrj.cloud.security.component;
+
+import cn.hutool.json.JSONUtil;
+import com.hwrj.cloud.common.api.CommonResult;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 自定义返回结果:未登录或登录过期
+ * Created by macro on 2018/5/14.
+ */
+public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
+        response.setHeader("Access-Control-Allow-Origin", "*");
+        response.setHeader("Cache-Control","no-cache");
+        response.setCharacterEncoding("UTF-8");
+        response.setContentType("application/json");
+        response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
+        response.getWriter().flush();
+    }
+}

+ 32 - 0
forest-security/src/main/java/com/hwrj/cloud/security/component/RestfulAccessDeniedHandler.java

@@ -0,0 +1,32 @@
+package com.hwrj.cloud.security.component;
+
+import cn.hutool.json.JSONUtil;
+import com.hwrj.cloud.common.api.CommonResult;
+import com.hwrj.cloud.common.exception.GlobalException;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 自定义返回结果:没有权限访问时
+ * Created by macro on 2018/4/26.
+ */
+public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
+    @Override
+    public void handle(HttpServletRequest request,
+                       HttpServletResponse response,
+                       AccessDeniedException e) throws IOException, ServletException {
+        response.setHeader("Access-Control-Allow-Origin", "*");
+        response.setHeader("Cache-Control","no-cache");
+        response.setCharacterEncoding("UTF-8");
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType("application/json;charset=UTF-8");
+        response.getWriter().print(JSONUtil.toJsonStr(CommonResult.forbidden(e.getMessage())));
+        response.getWriter().flush();
+        response.getWriter().close();
+    }
+}

+ 21 - 0
forest-security/src/main/java/com/hwrj/cloud/security/config/IgnoreUrlsConfig.java

@@ -0,0 +1,21 @@
+package com.hwrj.cloud.security.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 用于配置不需要保护的资源路径
+ * Created by macro on 2018/11/5.
+ */
+@Getter
+@Setter
+@ConfigurationProperties(prefix = "secure.ignored")
+public class IgnoreUrlsConfig {
+
+    private List<String> urls = new ArrayList<>();
+
+}

+ 63 - 0
forest-security/src/main/java/com/hwrj/cloud/security/config/RedisConfig.java

@@ -0,0 +1,63 @@
+package com.hwrj.cloud.security.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.time.Duration;
+
+/**
+ * Redis相关配置
+ * Created by macro on 2020/3/2.
+ */
+@EnableCaching
+@Configuration
+public class RedisConfig extends CachingConfigurerSupport {
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+        RedisSerializer<Object> serializer = redisSerializer();
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        redisTemplate.setValueSerializer(serializer);
+        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
+        redisTemplate.setHashValueSerializer(serializer);
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
+    }
+
+    @Bean
+    public RedisSerializer<Object> redisSerializer() {
+        //创建JSON序列化器
+        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        serializer.setObjectMapper(objectMapper);
+        return serializer;
+    }
+
+    @Bean
+    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
+        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
+        //设置Redis缓存有效期为1天
+        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
+                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1));
+        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
+    }
+
+}

+ 127 - 0
forest-security/src/main/java/com/hwrj/cloud/security/config/SecurityConfig.java

@@ -0,0 +1,127 @@
+package com.hwrj.cloud.security.config;
+
+import com.hwrj.cloud.security.component.*;
+import com.hwrj.cloud.security.util.JwtTokenUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+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.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+
+/**
+ * 对SpringSecurity的配置的扩展,支持自定义白名单资源路径和查询用户逻辑
+ * Created by macro on 2019/11/5.
+ */
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+
+    @Autowired(required = false)
+    private DynamicSecurityService dynamicSecurityService;
+
+    @Override
+    protected void configure(HttpSecurity httpSecurity) throws Exception {
+        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
+                .authorizeRequests();
+        //不需要保护的资源路径允许访问
+        for (String url : ignoreUrlsConfig().getUrls()) {
+            registry.antMatchers(url).permitAll();
+        }
+        //允许跨域请求的OPTIONS请求
+        registry.antMatchers(HttpMethod.OPTIONS)
+                .permitAll();
+        // 任何请求需要身份认证
+        registry.and()
+                .authorizeRequests()
+                .anyRequest()
+                .authenticated()
+                // 关闭跨站请求防护及不使用session
+                .and()
+                .csrf()
+                .disable()
+                .sessionManagement()
+                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                // 自定义权限拒绝处理类
+                .and()
+                .exceptionHandling()
+                .accessDeniedHandler(restfulAccessDeniedHandler())
+                .authenticationEntryPoint(restAuthenticationEntryPoint())
+                // 自定义权限拦截器JWT过滤器
+                .and()
+                .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
+        //有动态权限配置时添加动态权限校验过滤器
+        if(dynamicSecurityService!=null){
+            registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
+        }
+    }
+
+    @Override
+    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+        auth.userDetailsService(userDetailsService())
+                .passwordEncoder(passwordEncoder());
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    @Bean
+    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
+        return new JwtAuthenticationTokenFilter();
+    }
+
+    @Bean
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+    @Bean
+    public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
+        return new RestfulAccessDeniedHandler();
+    }
+
+    @Bean
+    public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
+        return new RestAuthenticationEntryPoint();
+    }
+
+    @Bean
+    public IgnoreUrlsConfig ignoreUrlsConfig() {
+        return new IgnoreUrlsConfig();
+    }
+
+    @Bean
+    public JwtTokenUtil jwtTokenUtil() {
+        return new JwtTokenUtil();
+    }
+
+    @ConditionalOnBean(name = "dynamicSecurityService")
+    @Bean
+    public DynamicAccessDecisionManager dynamicAccessDecisionManager() {
+        return new DynamicAccessDecisionManager();
+    }
+
+
+    @ConditionalOnBean(name = "dynamicSecurityService")
+    @Bean
+    public DynamicSecurityFilter dynamicSecurityFilter() {
+        return new DynamicSecurityFilter();
+    }
+
+    @ConditionalOnBean(name = "dynamicSecurityService")
+    @Bean
+    public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {
+        return new DynamicSecurityMetadataSource();
+    }
+
+}

+ 182 - 0
forest-security/src/main/java/com/hwrj/cloud/security/service/RedisService.java

@@ -0,0 +1,182 @@
+package com.hwrj.cloud.security.service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * redis操作Service
+ * Created by macro on 2020/3/3.
+ */
+public interface RedisService {
+
+    /**
+     * 保存属性
+     */
+    void set(String key, Object value, long time);
+
+    /**
+     * 保存属性
+     */
+    void set(String key, Object value);
+
+    /**
+     * 获取属性
+     */
+    Object get(String key);
+
+    /**
+     * 删除属性
+     */
+    Boolean del(String key);
+
+    /**
+     * 批量删除属性
+     */
+    Long del(List<String> keys);
+
+    /**
+     * 设置过期时间
+     */
+    Boolean expire(String key, long time);
+
+    /**
+     * 获取过期时间
+     */
+    Long getExpire(String key);
+
+    /**
+     * 判断是否有该属性
+     */
+    Boolean hasKey(String key);
+
+    /**
+     * 按delta递增
+     */
+    Long incr(String key, long delta);
+
+    /**
+     * 按delta递减
+     */
+    Long decr(String key, long delta);
+
+    /**
+     * 获取Hash结构中的属性
+     */
+    Object hGet(String key, String hashKey);
+
+    /**
+     * 向Hash结构中放入一个属性
+     */
+    Boolean hSet(String key, String hashKey, Object value, long time);
+
+    /**
+     * 向Hash结构中放入一个属性
+     */
+    void hSet(String key, String hashKey, Object value);
+
+    /**
+     * 直接获取整个Hash结构
+     */
+    Map<Object, Object> hGetAll(String key);
+
+    /**
+     * 直接设置整个Hash结构
+     */
+    Boolean hSetAll(String key, Map<String, Object> map, long time);
+
+    /**
+     * 直接设置整个Hash结构
+     */
+    void hSetAll(String key, Map<String, Object> map);
+
+    /**
+     * 删除Hash结构中的属性
+     */
+    void hDel(String key, Object... hashKey);
+
+    /**
+     * 判断Hash结构中是否有该属性
+     */
+    Boolean hHasKey(String key, String hashKey);
+
+    /**
+     * Hash结构中属性递增
+     */
+    Long hIncr(String key, String hashKey, Long delta);
+
+    /**
+     * Hash结构中属性递减
+     */
+    Long hDecr(String key, String hashKey, Long delta);
+
+    /**
+     * 获取Set结构
+     */
+    Set<Object> sMembers(String key);
+
+    /**
+     * 向Set结构中添加属性
+     */
+    Long sAdd(String key, Object... values);
+
+    /**
+     * 向Set结构中添加属性
+     */
+    Long sAdd(String key, long time, Object... values);
+
+    /**
+     * 是否为Set中的属性
+     */
+    Boolean sIsMember(String key, Object value);
+
+    /**
+     * 获取Set结构的长度
+     */
+    Long sSize(String key);
+
+    /**
+     * 删除Set结构中的属性
+     */
+    Long sRemove(String key, Object... values);
+
+    /**
+     * 获取List结构中的属性
+     */
+    List<Object> lRange(String key, long start, long end);
+
+    /**
+     * 获取List结构的长度
+     */
+    Long lSize(String key);
+
+    /**
+     * 根据索引获取List中的属性
+     */
+    Object lIndex(String key, long index);
+
+    /**
+     * 向List结构中添加属性
+     */
+    Long lPush(String key, Object value);
+
+    /**
+     * 向List结构中添加属性
+     */
+    Long lPush(String key, Object value, long time);
+
+    /**
+     * 向List结构中批量添加属性
+     */
+    Long lPushAll(String key, Object... values);
+
+    /**
+     * 向List结构中批量添加属性
+     */
+    Long lPushAll(String key, Long time, Object... values);
+
+    /**
+     * 从List结构中移除属性
+     */
+    Long lRemove(String key, long count, Object value);
+}

+ 199 - 0
forest-security/src/main/java/com/hwrj/cloud/security/service/impl/RedisServiceImpl.java

@@ -0,0 +1,199 @@
+package com.hwrj.cloud.security.service.impl;
+
+import com.hwrj.cloud.security.service.RedisService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * redis操作实现类
+ * Created by macro on 2020/3/3.
+ */
+@Service
+public class RedisServiceImpl implements RedisService {
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Override
+    public void set(String key, Object value, long time) {
+        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void set(String key, Object value) {
+        redisTemplate.opsForValue().set(key, value);
+    }
+
+    @Override
+    public Object get(String key) {
+        return redisTemplate.opsForValue().get(key);
+    }
+
+    @Override
+    public Boolean del(String key) {
+        return redisTemplate.delete(key);
+    }
+
+    @Override
+    public Long del(List<String> keys) {
+        return redisTemplate.delete(keys);
+    }
+
+    @Override
+    public Boolean expire(String key, long time) {
+        return redisTemplate.expire(key, time, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public Long getExpire(String key) {
+        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public Boolean hasKey(String key) {
+        return redisTemplate.hasKey(key);
+    }
+
+    @Override
+    public Long incr(String key, long delta) {
+        return redisTemplate.opsForValue().increment(key, delta);
+    }
+
+    @Override
+    public Long decr(String key, long delta) {
+        return redisTemplate.opsForValue().increment(key, -delta);
+    }
+
+    @Override
+    public Object hGet(String key, String hashKey) {
+        return redisTemplate.opsForHash().get(key, hashKey);
+    }
+
+    @Override
+    public Boolean hSet(String key, String hashKey, Object value, long time) {
+        redisTemplate.opsForHash().put(key, hashKey, value);
+        return expire(key, time);
+    }
+
+    @Override
+    public void hSet(String key, String hashKey, Object value) {
+        redisTemplate.opsForHash().put(key, hashKey, value);
+    }
+
+    @Override
+    public Map<Object, Object> hGetAll(String key) {
+        return redisTemplate.opsForHash().entries(key);
+    }
+
+    @Override
+    public Boolean hSetAll(String key, Map<String, Object> map, long time) {
+        redisTemplate.opsForHash().putAll(key, map);
+        return expire(key, time);
+    }
+
+    @Override
+    public void hSetAll(String key, Map<String, Object> map) {
+        redisTemplate.opsForHash().putAll(key, map);
+    }
+
+    @Override
+    public void hDel(String key, Object... hashKey) {
+        redisTemplate.opsForHash().delete(key, hashKey);
+    }
+
+    @Override
+    public Boolean hHasKey(String key, String hashKey) {
+        return redisTemplate.opsForHash().hasKey(key, hashKey);
+    }
+
+    @Override
+    public Long hIncr(String key, String hashKey, Long delta) {
+        return redisTemplate.opsForHash().increment(key, hashKey, delta);
+    }
+
+    @Override
+    public Long hDecr(String key, String hashKey, Long delta) {
+        return redisTemplate.opsForHash().increment(key, hashKey, -delta);
+    }
+
+    @Override
+    public Set<Object> sMembers(String key) {
+        return redisTemplate.opsForSet().members(key);
+    }
+
+    @Override
+    public Long sAdd(String key, Object... values) {
+        return redisTemplate.opsForSet().add(key, values);
+    }
+
+    @Override
+    public Long sAdd(String key, long time, Object... values) {
+        Long count = redisTemplate.opsForSet().add(key, values);
+        expire(key, time);
+        return count;
+    }
+
+    @Override
+    public Boolean sIsMember(String key, Object value) {
+        return redisTemplate.opsForSet().isMember(key, value);
+    }
+
+    @Override
+    public Long sSize(String key) {
+        return redisTemplate.opsForSet().size(key);
+    }
+
+    @Override
+    public Long sRemove(String key, Object... values) {
+        return redisTemplate.opsForSet().remove(key, values);
+    }
+
+    @Override
+    public List<Object> lRange(String key, long start, long end) {
+        return redisTemplate.opsForList().range(key, start, end);
+    }
+
+    @Override
+    public Long lSize(String key) {
+        return redisTemplate.opsForList().size(key);
+    }
+
+    @Override
+    public Object lIndex(String key, long index) {
+        return redisTemplate.opsForList().index(key, index);
+    }
+
+    @Override
+    public Long lPush(String key, Object value) {
+        return redisTemplate.opsForList().rightPush(key, value);
+    }
+
+    @Override
+    public Long lPush(String key, Object value, long time) {
+        Long index = redisTemplate.opsForList().rightPush(key, value);
+        expire(key, time);
+        return index;
+    }
+
+    @Override
+    public Long lPushAll(String key, Object... values) {
+        return redisTemplate.opsForList().rightPushAll(key, values);
+    }
+
+    @Override
+    public Long lPushAll(String key, Long time, Object... values) {
+        Long count = redisTemplate.opsForList().rightPushAll(key, values);
+        expire(key, time);
+        return count;
+    }
+
+    @Override
+    public Long lRemove(String key, long count, Object value) {
+        return redisTemplate.opsForList().remove(key, count, value);
+    }
+}

+ 170 - 0
forest-security/src/main/java/com/hwrj/cloud/security/util/JwtTokenUtil.java

@@ -0,0 +1,170 @@
+package com.hwrj.cloud.security.util;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JwtToken生成的工具类
+ * JWT token的格式:header.payload.signature
+ * header的格式(算法、token的类型):
+ * {"alg": "HS512","typ": "JWT"}
+ * payload的格式(用户名、创建时间、生成时间):
+ * {"sub":"wang","created":1489079981393,"exp":1489684781}
+ * signature的生成算法:
+ * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
+ * Created by macro on 2018/4/26.
+ */
+public class JwtTokenUtil {
+    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
+    private static final String CLAIM_KEY_USERNAME = "sub";
+    private static final String CLAIM_KEY_CREATED = "created";
+    @Value("${jwt.secret}")
+    private String secret;
+    @Value("${jwt.expiration}")
+    private Long expiration;
+    @Value("${jwt.tokenHead}")
+    private String tokenHead;
+
+    /**
+     * 根据负责生成JWT的token
+     */
+    private String generateToken(Map<String, Object> claims) {
+        return Jwts.builder()
+                .setClaims(claims)
+                .setExpiration(generateExpirationDate())
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .compact();
+    }
+
+    /**
+     * 从token中获取JWT中的负载
+     */
+    private Claims getClaimsFromToken(String token) {
+        Claims claims = null;
+        try {
+            claims = Jwts.parser()
+                    .setSigningKey(secret)
+                    .parseClaimsJws(token)
+                    .getBody();
+        } catch (Exception e) {
+            LOGGER.info("JWT格式验证失败:{}", token);
+        }
+        return claims;
+    }
+
+    /**
+     * 生成token的过期时间
+     */
+    private Date generateExpirationDate() {
+        return new Date(System.currentTimeMillis() + expiration * 1000);
+    }
+
+    /**
+     * 从token中获取登录用户名
+     */
+    public String getUserNameFromToken(String token) {
+        String username;
+        try {
+            Claims claims = getClaimsFromToken(token);
+            username = claims.getSubject();
+        } catch (Exception e) {
+            username = null;
+        }
+        return username;
+    }
+
+    /**
+     * 验证token是否还有效
+     *
+     * @param token       客户端传入的token
+     * @param userDetails 从数据库中查询出来的用户信息
+     */
+    public boolean validateToken(String token, UserDetails userDetails) {
+        String username = getUserNameFromToken(token);
+        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
+    }
+
+    /**
+     * 判断token是否已经失效
+     */
+    private boolean isTokenExpired(String token) {
+        Date expiredDate = getExpiredDateFromToken(token);
+        return expiredDate.before(new Date());
+    }
+
+    /**
+     * 从token中获取过期时间
+     */
+    private Date getExpiredDateFromToken(String token) {
+        Claims claims = getClaimsFromToken(token);
+        return claims.getExpiration();
+    }
+
+    /**
+     * 根据用户信息生成token
+     */
+    public String generateToken(UserDetails userDetails) {
+        Map<String, Object> claims = new HashMap<>();
+        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
+        claims.put(CLAIM_KEY_CREATED, new Date());
+        return generateToken(claims);
+    }
+
+    /**
+     * 当原来的token没过期时是可以刷新的
+     *
+     * @param oldToken 带tokenHead的token
+     */
+    public String refreshHeadToken(String oldToken) {
+        if(StrUtil.isEmpty(oldToken)){
+            return null;
+        }
+        String token = oldToken.substring(tokenHead.length());
+        if(StrUtil.isEmpty(token)){
+            return null;
+        }
+        //token校验不通过
+        Claims claims = getClaimsFromToken(token);
+        if(claims==null){
+            return null;
+        }
+        //如果token已经过期,不支持刷新
+        if(isTokenExpired(token)){
+            return null;
+        }
+        //如果token在30分钟之内刚刷新过,返回原token
+        if(tokenRefreshJustBefore(token,30*60)){
+            return token;
+        }else{
+            claims.put(CLAIM_KEY_CREATED, new Date());
+            return generateToken(claims);
+        }
+    }
+
+    /**
+     * 判断token在指定时间内是否刚刚刷新过
+     * @param token 原token
+     * @param time 指定时间(秒)
+     */
+    private boolean tokenRefreshJustBefore(String token, int time) {
+        Claims claims = getClaimsFromToken(token);
+        Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
+        Date refreshDate = new Date();
+        //刷新时间在创建时间的指定时间内
+        if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){
+            return true;
+        }
+        return false;
+    }
+}

+ 44 - 0
forest-security/src/main/java/com/hwrj/cloud/security/util/SpringUtil.java

@@ -0,0 +1,44 @@
+package com.hwrj.cloud.security.util;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+/**
+ * Spring工具类
+ * Created by macro on 2020/3/3.
+ */
+@Component
+public class SpringUtil implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+
+    // 获取applicationContext
+    public static ApplicationContext getApplicationContext() {
+        return applicationContext;
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        if (SpringUtil.applicationContext == null) {
+            SpringUtil.applicationContext = applicationContext;
+        }
+    }
+
+    // 通过name获取Bean
+    public static Object getBean(String name) {
+        return getApplicationContext().getBean(name);
+    }
+
+    // 通过class获取Bean
+    public static <T> T getBean(Class<T> clazz) {
+        return getApplicationContext().getBean(clazz);
+    }
+
+    // 通过name,以及Clazz返回指定的Bean
+    public static <T> T getBean(String name, Class<T> clazz) {
+        return getApplicationContext().getBean(name, clazz);
+    }
+
+}