【Spring】SpringSecurity基础原理与使用

说明

抽空再学习了一下SpringSecurity。本章结合基础代码做一个基础总结。本章使用的是SpringBoot2.5.0 版本下的spring-boot-starter-security版本5.5.0。原理内容还不完善,后续有机会再补充。

参考了B站视频官网的相关介绍,感兴趣的可以去看看。代码下载。

基础原理

概述

SpringSecurity默认自动实现过滤器链Bean SpringSecurityFilterChain,该对象负责所有安全性。学习SpringSecurityFilterChain过滤器链的基础流程,理解SpringSecuriy的基础原理。

FilterChain

在Spring MVC应用程序中,Servlet是DispatcherServlet的一个实例。最多一个Servlet可以处理单个HttpServletRequest和HttpServletResponse。但是,可以使用多个Filter来:

  • 阻止调用下游过滤器或Servlet。在这中,过滤器通常会写入HttpServletResponse;
  • 修改下游过滤器和Servlet使用的HttpServletRequest或HttpServletResponse;
    在这里插入图片描述

FilterChainProxy

  • FilterChainProxy是SpringSecurity提供的一个特殊的Filter,它允许通过SecurityFilterChain委托给多个Filter实例;
  • 由于FilterChainProxy是一个Bean,它通常被包装在DelegatingFilterProxy中;
  • FilterChainProxy可以确定应该使用哪个SecurityFilterChain。这允许为应用程序的不同部分提供完全独立的配置;
    在这里插入图片描述

SecurityFilterChain

  • SecurityFilterChain是被FilterChainProxy调用使用的,包含了多个SecurityFilter。SecurityFilter的执行顺序是很重要的,SecurityFilter有很多,对应着不同的功能,在这不做过多介绍。
    在这里插入图片描述

Security Filters(略、待补充)

  • UsernamePasswordAuthenticationFilter 用户名密码表单认证过滤器
  • DigestAuthenticationFilter 处理HTTP请求的Digest授权头,将结果放入SecurityContextHolder中。
  • BasicAuthenticationFilter 处理HTTP请求的BASIC授权头,并将结果放入SecurityContextHolder中
  • ExceptionTranslationFilter
    异常处理过滤器(认证异常AuthenticationException与授权异常AccessDeniedException)
    允许将AccessDeniedException和AuthenticationException转换为HTTP响应。
    在这里插入图片描述
  1. 首先,ExceptionTranslationFilter调用FilterChain.doFilter(request,response)来调用应用程序的其余部分。
  2. 如果用户没有经过身份验证,或者它是一个AuthenticationException,那么开始身份验证:
    1.SecurityContexttHolder被清除
    2.HttpServletRequest被保存在RequestCache中。当用户成功通过身份验证时,将使用RequestCache重放原始请求。
    3.AuthenticationEntryPoint用于从客户端请求凭据。例如,它可能重定向到登录页面或发送WWW-Authenticate报头。
  3. 如果它是一个AccessDeniedException,那么AccessDenied调用AccessDeniedHandler来处理被拒绝的访问。如果应用程序不抛出AccessDeniedException或AuthenticationException,则ExceptionTranslationFilter不做任何事情。

认证

一.容器

1.SecurityContextHolder

  • 存储身份验证者的详细信息,SpringSecurity的认证模型的核心。
  • 它包含SecurityContext。SecurityContextHolder是Spring Security存储身份验证对象的详细信息的地方,表示用户通过身份验证的最简单方法是直接设置SecurityContextHolder。
    在这里插入图片描述
SecurityContext context = SecurityContextHolder.createEmptyContext(); 
Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);

> - 我们首先创建一个空的SecurityContext。创建一个新的SecurityContext实例,而不是使用SecurityContextHolder.getContext().
> - 接下来,我们创建一个新的Authentication对象。Spring Security不关心SecurityContext上设置的认证实现类型。这里我们使用TestingAuthenticationToken,因为它非常简单。更常见的生产场景是UsernamePasswordAuthenticationToken(userDetails、密码、权限)> - setAuthentication (authentication)来避免多个线程之间的竞争条件是很重要的。
> - 最后,我们在SecurityContextHolder上设置SecurityContextSpring Security将使用此信息进行授权。
> - 如果您希望获得关于已验证主体的信息,可以通过访问SecurityContextHolder来实现。

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

2.SecurityContext

从SecurityContexttHolder中获取,并包含当前经过身份验证的用户的身份验证

3.Authentication

两个主要使用场景:

  • AuthenticationManager的输入,用于提供用户为进行身份验证所提供的凭据。在此场景中使用时,isAuthenticated()返回false。
  • 表示当前经过身份验证的用户。当前的认证可以从SecurityContext中获得。

包含:

  • principal:标识用户。当使用用户名/密码进行身份验证时,这通常是UserDetails的一个实例。
  • credentials :通常是密码。在许多情况下,这将在用户经过身份验证后被清除,以确保它不会泄露。
  • authorities:Grantedauthorys是授予用户的高级权限。一些例子是角色或作用域。

4.GrantedAuthority

在身份验证中授予主体的权限(即角色、范围等)。

二.执行流程API

1.AuthenticationManager接口

定义SpringSecurity的过滤器如何执行身份验证的API。

2.ProviderManager

  • 最常见的AuthenticationManager实现
  • ProviderManager包含一个AuthenticationProvider集合。 private List<AuthenticationProvider> providers = Collections.emptyList();
  • 每个AuthenticationProvider都有机会表明身份验证应该成功、失败,或者表明它不能做出决定,并允许下游的AuthenticationProvider做出决定。
  • 如果配置的AuthenticationProviders中没有一个可以进行身份验证,那么身份验证将会失败,并带有一个ProviderNotFoundException,这是一个特殊的AuthenticationException,表示没有配置ProviderManager来支持传递给它的身份验证类型。
    在这里插入图片描述
  • 在实践中,每个AuthenticationProvider都知道如何执行特定类型的身份验证。例如,一个AuthenticationProvider可能能够验证用户名/密码,而另一个可能能够验证SAML断言。这允许每个AuthenticationProvider执行非常特定类型的身份验证,同时支持多种类型的身份验证,并且只公开一个AuthenticationManager bean。

  • ProviderManager还允许配置一个可选的父AuthenticationManager,当没有AuthenticationProvider可以执行身份验证时,该父AuthenticationManager将被执行。父类可以是任何类型的AuthenticationManager,但它通常是ProviderManager的一个实例。
    在这里插入图片描述

  • 事实上,多个ProviderManager实例可能共享同一个父AuthenticationManager。这在有多个SecurityFilterChain实例,它们有一些共同的身份验证(共享的父AuthenticationManager),但也有不同的身份验证机制(不同的ProviderManager实例)的场景中比较常见。
    在这里插入图片描述

3.AuthenticationProvider接口

  • ProviderManager用于执行特定类型的身份验证
  • 可以将多个AuthenticationProviders注入到ProviderManager中。每个AuthenticationProvider执行特定类型的身份验证。例如,DaoAuthenticationProvider支持基于用户名/密码的身份验证,而JwtAuthenticationProvider支持对JWT令牌进行身份验证。

4.DaoAuthenticationProvider

  • DaoAuthenticationProvider是一个AuthenticationProvider的实现,它利用UserDetailsServicePasswordEncoder来验证用户名和密码。

UserDetailsService

  • UserDetailsServiceDaoAuthenticationProvider用于检索用户名、密码和其他属性,用于对用户名和密码进行身份验证。Spring Security提供了UserDetailsService的内存和JDBC实现。
  • 您可以通过将自定义UserDetailsService公开为bean来定义自定义身份验证。例如,假设MyUserDetailServiceImpl实现了UserDetailsService,下面将自定义身份验证:
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {
    
     ... }

UserDetails

  • UserDetails由UserDetailsService返回。DaoAuthenticationProvider验证UserDetails,然后返回一个身份验证,该身份验证的主体是由配置的UserDetailsService返回的UserDetails。

PasswordEncoder

  • Spring Security的servlet通过与PasswordEncoder集成,支持安全存储密码。定制Spring Security使用的PasswordEncoder实现可以通过公开PasswordEncoder Bean来完成。
    //创建BCryptPasswordEncoder注入容器中,系统默认使用注入的PasswordEncoder进行加密和校验
    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

5.AbstractAuthenticationProcessingFilter

  • 用于身份验证的基本过滤器。这也让我们很好地了解了身份验证的高级流程以及各个部分如何一起工作。
    在这里插入图片描述
  1. 当用户提交他们的凭证时,AbstractAuthenticationProcessingFilter从HttpServletRequest创建一个Authentication。创建的Authentication类型取决于AbstractAuthenticationProcessingFilter的子类。例如,UsernamePasswordAuthenticationFilter从HttpServletRequest中提交的用户名和密码创建一个UsernamePasswordAuthenticationToken
  2. Authentication被传递到AuthenticationManager进行身份验证。
  3. 如果认证失败,则Failure:
    SecurityContextHolder被清除;
    RememberMeServices调用loginFail。如果没有配置remember me,这是一个无操作;
    调用AuthenticationFailureHandler;
  4. 如果身份验证成功,则Success:
    SessionAuthenticationStrategy收到新登录的通知;
    Authentication设置在SecurityContextHolder上。之后,SecurityContextPersistenceFilter将SecurityContext保存到HttpSession中;
    RememberMeServices调用loginFail。如果没有配置remember me,这是一个无操;
    ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent;
    调用AuthenticationSuccessHandler;

6.UsernamePasswordAuthenticationFilter

AbstractAuthenticationProcessingFilter抽象类的实现。处理身份验证表单提交。登录表单必须向这个筛选器提供两个参数:用户名和密码;
从HttpServletRequest中提取用户名和密码,创建了UsernamePasswordAuthenticationToken(一种Authentication);
接下来,UsernamePasswordAuthenticationToken被传递到AuthenticationManager以进行身份验证。AuthenticationManager的details取决于用户信息的存储方式。
在这里插入图片描述

授权(略、待补充)

GrantedAuthority接口

在认证中提到,Authentication实现了存储一个GrantedAuthority集合对象,GrantedAuthority集合对象代表了在身份验证中授予主体的权限(即角色、范围等),通过AuthenticationManager,GrantedAuthority对象被加入到Authentication,然后由AccessDecisionManager在做出授权决策时读取。

  • GrantedAuthority是一个只有一个方法 String getAuthority() 的接口;
  • 此方法允许AccessDecisionManager获得被授予权限的精确字符串表示。通过返回一个字符串的表示,一个被授予的权限可以被大多数AccessDecisionManager轻松地“读取”。如果一个被授予的权限不能被精确地表示为一个字符串,那么这个被授予的权限就会被认为是“复杂的”,getAuthority()必须返回null。

SimpleGrantedAuthority

GrantedAuthority一个具体的授权实现,SimpleGrantedAuthority。这允许将任何用户指定的字符串转换为授权权限。安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority来填充Authentication对象。

基础使用

创建

创建项目
在这里插入图片描述
导入依赖
springBoot版本2.5.0 下 spring-boot-starter-security中版本5.5.0
在这里插入图片描述

	 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--security启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>

        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

创建启动类App

@SpringBootApplication
@MapperScan("com.lingfei.mapper")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class App {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(App.class,args);
    }
}

创建配置文件application.yml

server:
  port: 10001
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/lingfei?useUnicode=true&characterEncoding=utf-8
    username: root
    password: xieji
    driver-class-name: com.mysql.jdbc.Driver
  redis:
    password: 123456

创建测试Controller
在这里插入图片描述

浏览器访问 http://localhost:10001/test 自动跳转 http://localhost:8889/login
在这里插入图片描述

工具类:

1.FastJsonRedisSerializer

public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
    
    

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;


    /**
     * 添加autotype白名单
     * 解决redis反序列化对象时报错 :com.alibaba.fastjson.JSONException: autoType is not support
     */
    static {
    
    
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz) {
    
    
        super();
        this.clazz = clazz;
    }

    public byte[] serialize(T t) throws SerializationException {
    
    
        if (t == null) {
    
    
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    public T deserialize(byte[] bytes) throws SerializationException {
    
    
        if (bytes == null || bytes.length <= 0) {
    
    
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }
}

2.JWTUtils

//JWT工具类
public class JWTUtils {
    
    
    //密钥
    private static final String SALT = "kxoif%$*hdas$@_dlsd";
    //过期时间
    private static final Integer EXPIRE_TIME = 1000 * 60 * 30;

    //生成token
    public static String getToken(String id, String name) {
    
    
        String token = Jwts.builder()
                //设置开始时间
                .setIssuedAt(new Date(System.currentTimeMillis()))
                //设置有效期
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME))
                //设置用户声明
                .claim("id", id)
                .claim("name", name)
                //签名
                .signWith(SignatureAlgorithm.HS256, SALT)
                .compact();
        System.out.println(token);
        return token;
    }

    //验证token
    public static boolean isExpire(String token) {
    
    
        if (!StringUtils.hasText(token)) {
    
    
            return true;
        }
        try {
    
    
            Jwts.parser().setSigningKey(SALT).parseClaimsJws(token);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return true;
        }
        return false;
    }

    public static boolean isExpire(HttpServletRequest request) {
    
    
        String token = request.getHeader("token");
        if (StringUtils.hasLength(token)) {
    
    
            return false;
        }
        try {
    
    
            Jwts.parser().setSigningKey(SALT).parseClaimsJws(token);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return true;
        }
        return false;
    }

    //获取用户id
    public static String getId(String token) {
    
    
        try {
    
    
            Claims body = Jwts.parser().setSigningKey(SALT).parseClaimsJws(token).getBody();
            return (String) body.get("id");
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return null;
        }
    }

    public String getId(HttpServletRequest request) {
    
    
        String token = request.getHeader("token");
        Claims body = Jwts.parser().setSigningKey(SALT).parseClaimsJws(token).getBody();
        return (String) body.get("id");
    }

}

3.RedisCache

@SuppressWarnings(value = {
    
    "unchecked", "rawtypes"})
@Component
public class RedisCache {
    
    
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value) {
    
    
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
    
    
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
    
    
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
    
    
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key) {
    
    
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key) {
    
    
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection) {
    
    
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
    
    
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
    
    
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
    
    
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext()) {
    
    
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key) {
    
    
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
    
    
        if (dataMap != null) {
    
    
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key) {
    
    
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
    
    
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
    
    
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey) {
    
    
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
    
    
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern) {
    
    
        return redisTemplate.keys(pattern);
    }
}

4.WebUtils

public class WebUtils
{
    
    
    /**
     * 将字符串渲染到客户端
     * 
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
    
    
        try
        {
    
    
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
    
    
            e.printStackTrace();
        }
        return null;
    }
}

5.ServiceResult

/**
 * service层返回对象列表封装
 *
 * @param <T>
 */
public class ServiceResult<T> implements Serializable {
    
    

    private boolean success = false;

    private String code;

    private String message;

    private T result;

    private ServiceResult() {
    
    
    }

    public static <T> ServiceResult<T> success(T result) {
    
    
        ServiceResult<T> item = new ServiceResult<T>();
        item.success = true;
        item.result = result;
        item.code = "0";
        item.message = "success";
        return item;
    }

    public static <T> ServiceResult<T> failure(String errorCode, String errorMessage) {
    
    
        ServiceResult<T> item = new ServiceResult<T>();
        item.success = false;
        item.code = errorCode;
        item.message = errorMessage;
        return item;
    }

    public static <T> ServiceResult<T> failure(int errorCode, String errorMessage) {
    
    
        ServiceResult<T> item = new ServiceResult<T>();
        item.success = false;
        item.code = errorCode + "";
        item.message = errorMessage;
        return item;
    }

    public boolean hasResult() {
    
    
        return result != null;
    }

    public boolean isSuccess() {
    
    
        return success;
    }

    public T getResult() {
    
    
        return result;
    }

    public String getCode() {
    
    
        return code;
    }

    public String getMessage() {
    
    
        return message;
    }

}

基础配置
1.RedisConfig

@Configuration
public class RedisConfig {
    
    

    @Bean(name = "redisTemplate")
    @SuppressWarnings(value = {
    
    "unchecked", "rawtypes"})
    public RedisTemplate<String, Object> fastJsonRedisTemplate(RedisConnectionFactory factory) {
    
    
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(fastJsonRedisSerializer);

        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(fastJsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

2.CorsConfig

//跨域配置 spring boot
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    

    @Override
    public void addCorsMappings(CorsRegistry registry) {
    
    
        //设置允许跨域请求的域名
        registry.addMapping("/**")
                //设置运行跨域请求的域名
                .allowedOriginPatterns("*")
                //是否运行cookie
                .allowCredentials(true)
                //设置允许请求的方式
                .allowedMethods(new String[]{
    
    "GET", "POST", "PUT", "DELETE"})
                //设置允许的header属性
                .allowedHeaders("*")
                //跨域允许时间
                .maxAge(3600);
    }
}

3.SecurityConfig

/**
 *
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

//    @Autowired
//    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

//    @Autowired
//    private AuthenticationEntryPoint authenticationEntryPoint;
//    @Autowired
//    private AccessDeniedHandler accessDeniedHandler;

    //创建BCryptPasswordEncoder注入容器中,系统默认使用注入的PasswordEncoder进行加密和校验
    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    

        http.csrf().disable().//关闭csrf  csrf攻击 跨站请求伪造
                sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                // login匿名访问 不需要权限
                .antMatchers("/login").anonymous()
                //通过配置 访问权限 需要权限
                .antMatchers("/test").hasAuthority("system:test:listTest")
                // 除上面的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //定义登录过滤器位置顺序
//        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //定义异常处理 认证、授权异常
//        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);

        //允许跨域 跨域配置 spring security
        http.cors();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }


}

创建基础的实体类、Mapper、数据库表
User:

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class User  implements Serializable {
    
    
    private static final long serialVersionUID = -7346780360611264120L;
    @TableId
    private Long id;
    private String username;
    private String password;
}

public interface UserMapper extends BaseMapper<User>{
    
    
}

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

Role:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    
    
    @TableId
    private Long id;
    private String name;
    private String roleKey;
}

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `role_key` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

Menu :

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Menu implements Serializable {
    
    
    private static final long serialVersionUID = -5092761774075344489L;
    @TableId
    private Long id;
    private String name;
    private String authority;
}

public interface MenuMapper extends BaseMapper<Menu>{
    
    

    @Select("SELECT authority FROM `menu` WHERE id in (SELECT menu_id FROM `role_menu` where role_id in (SELECT role_id FROM `user_role` WHERE user_id = #{userId}));")
    List<String> selectMenusByUserId(@Param("userId") Long userId);
}

CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `authority` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

RoleMenu:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoleMenu {
    
    
    private Long roleId;
    private Long MenuId;
}
CREATE TABLE `role_menu` (
  `role_id` int(11) DEFAULT NULL,
  `menu_id` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

UserRole:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRole {
    
    
    private Long UserId;
    private Long roleId;
}
CREATE TABLE `user_role` (
  `user_id` int(11) DEFAULT NULL,
  `role_id` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

MyUserDetails

/**
 *
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyUserDetails implements UserDetails {
    
    

    private User user;

    private List<String> permissions;

    //不会被序列化
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    public MyUserDetails(User user, List<String> permissions) {
    
    
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        if (authorities != null) return authorities;

        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
    
    
        return user.getPassword();
    }

    @Override
    public String getUsername() {
    
    
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    
    
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
}

认证

MyUserDetailServiceImpl

/**
 *
 */
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {
    
    
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    
    
        //查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername,s);
        User user = userMapper.selectOne(queryWrapper);
        if(Objects.isNull((user))){
    
    
            throw new RuntimeException("用户名、密码错误!");
        }

        //设置用户权限信息
        List<String> authority = menuMapper.selectMenusByUserId(user.getId());

        //封装userDetails
        UserDetails userDetails = new MyUserDetails(user,authority);
        return userDetails;
    }
}

LoginController

@RestController
public class LoginController {
    
    

    @Autowired
    private LoginService loginService;

    @PostMapping("/login")
    public ServiceResult login(@RequestBody User user) {
    
    
        return loginService.login(user);
    }

    @GetMapping("/loginOut")
    public ServiceResult loginOut() {
    
    
        return loginService.loginOut();
    }

    @GetMapping("/test")
    public ServiceResult test() {
    
    
        return ServiceResult.success("OK");
    }

    @GetMapping("/testHasAuthority")
    @PreAuthorize("@MyAuthorityService.hasAuthority('system:test:list')")  //自定义权限认证
//    @PreAuthorize("hasAuthority('system:test:list')") //默认配置权限认证
//    @PreAuthorize("hasAnyAuthority('xx')")
//    @PreAuthorize("hasRole('xx')")
//    @PreAuthorize("hasAnyRole('xx')")
    public ServiceResult testHasAuthority() {
    
    
        return ServiceResult.success("testHasAuthority");
    }
}

LoginService

public interface LoginService {
    
    
    ServiceResult login(User user);

    ServiceResult loginOut();
}

LoginServiceImpl

@Service
public class LoginServiceImpl implements LoginService {
    
    

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public ServiceResult login(User user) {
    
    
        //authenticationManager authenticate进行用户认证
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        if (Objects.isNull(authenticate)) {
    
    
            throw new RuntimeException("登录失败!");
        }

        //生成JWT
        MyUserDetails myUserDetails = (MyUserDetails) authenticate.getPrincipal();
        String id = myUserDetails.getUser().getId().toString();
        String username = myUserDetails.getUser().getUsername();
        String token = JWTUtils.getToken(id, username);

        //保存到Redis
        redisCache.setCacheObject("myUserDetails"+id, myUserDetails);

        HashMap<Object, Object> resultMap = new HashMap<>();
        resultMap.put("token", token);
        return ServiceResult.success(resultMap);
    }

    @Override
    public ServiceResult loginOut() {
    
    
        //获取SecurityContextHolder中对应的用户信息
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();
        Long userId = myUserDetails.getUser().getId();

        //删除redis中对应的key
        redisCache.deleteObject("myUserDetails"+userId);

        return ServiceResult.success("loginOut success");
    }
}

JwtAuthenticationTokenFilter

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    
    
        System.out.println("start JwtAuthenticationTokenFilter===========");


        //获取Token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)) {
    
    
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        //解析Token
        String id = JWTUtils.getId(token);
        if (StringUtils.hasText(id)) {
    
    
            //从redis中获取用户信息
            MyUserDetails userDetails = redisCache.getCacheObject("myUserDetails" + id);
            if (Objects.isNull(userDetails)) {
    
    
                throw new RuntimeException("用户未登录");
            }
            //存入SecurityContextHolder ******
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

授权

自定义权限认证

使用见LoginController的testHasAuthority方法@PreAuthorize

public interface MyAuthorityService {
    
    
      ///自定义访问权限处理逻辑
      boolean hasAuthority(String authority);
}

@Service("MyAuthorityService")
public class MyAuthorityServiceImpl implements MyAuthorityService {
    
    

    //自定义访问权限处理逻辑
    @Override
    public boolean hasAuthority(String authority) {
    
    
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();
        List<String> permissions = myUserDetails.getPermissions();

        return permissions.contains(authority);
    }

}

认证与授权异常处理器

配置见SecurityConfig配置类

AuthenticationEntryPointImpl

//登陆认证失败
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
    
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
    
    
        //处理异常
        ServiceResult<Object> failure = ServiceResult.failure(HttpStatus.UNAUTHORIZED.value(), "认证失败");
        String result = JSON.toJSONString(failure);
        WebUtils.renderString(httpServletResponse,result);
    }
}

AccessDeniedHandlerImpl

//权限授权失败
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
    
        //处理异常
        ServiceResult<Object> failure = ServiceResult.failure(HttpStatus.NOT_ACCEPTABLE.value(), "授权失败");
        String result = JSON.toJSONString(failure);
        WebUtils.renderString(response,result);
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_42029283/article/details/128201125
今日推荐