Spring Security的学习

Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。如同Shiro一样,安全框架最重要的就是用户认证(Authentication)和用户授权 (Authorization)两个部分。实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。 相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。 自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security

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

引入依赖之后,启动项目就可以访问localhost:8080,默认的用户名:user

密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都回发生变化!

Spring Security 基础类

基础的Filter

SpringSecurity 采用的是责任链的设计模式,它本质是一条很长的过滤器链,这里有几个比较重要,会在配置中使用到的Filter

  • FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部,Spring MVC控制器的前面,正如其名Interceptor(拦截器)。虽然这个类名称中带有拦截器,但是它也实现了Filter接口。它会检查前面过滤器链中是否已通过,通过之后才会调用后台服务
  • ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
  • UsernamePasswordAuthenticationFilter:对/login 的 POST 请求(可配置)做拦截,校验表单中用户名,密码,在自定义认证Filter时就需要继承这个类
  • BasicAuthenticationFilter:授权过滤器,会将用户对应的权限列表放入Spring Security上下文(SecurityContextHolder.getContext().setAuthentication())中,在自定义认证Filter时就需要继承这个类

UserDetailsService 接口

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。 如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可

这个接口只有一个方法(loadUserByUsername()),也就是根据用户名获取User,我们只需要重写这个方法,根据参数(用户名)去数据库中查询

UserDetails 接口

在上面loadUserByUsername()方法返回一个UserDetails,这个类在Spring Security中代表用户主体,它定义的代码如下

它有一个实现类org.springframework.security.core.userdetails.User,我们在重写loadUserByUsername方法时就可以直接返回这个User对象(new User()),或者通过继承User类重写所需方法来定制UserDetails

PasswordEncoder 接口

这个接口用于密码验证,通过定义加密,解密方法可以完成用户认证时的密码处理。所以这个接口主要就是定义加密,解密方法

BCryptPasswordEncoSpringSecurity Web 权限方案der 是这个接口的一个实现类,也是Spring Security 官方推荐的密码解析器,采用bcrypt 强散列加密实现。是基于 Hash 算法实现的单向加密算法。可以通过 strength 控制加密强度,默认 10

SpringSecurity Web 权限方案

设置登录系统的账号、密码

方式一:在 application.properties配置文件中配置

spring.security.user.name=lz
spring.security.user.password=123

方式二:编写配置类实现configure接口

在配置类定义的用户名,密码优先级高于配置文件中配置的

一个简单的配置类

package com.lynu.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 一个最简单的配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 创建一个基于BCrypt算法的密码加密器(定义密码加密器必不可少)
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        // 设置
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("lz").password(password).roles("admin");
    }
}

方式三:在自定义配置类基础上再实现userDetailsService接口查询数据库

方式一,方式二都是将用户名,密码硬编码在配置文件或代码中,在实际项目中肯定是要去查询数据

一个较为全面的配置类

@Configuration
@EnableWebSecurity
// 开启Spring Security注解支持
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 注入自定义的userDetailsService去查询数据库
    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 设置密码处理器
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    // 注入数据源用于rememberMe自动创建表以及将记住的用户保存在数据在
    @Autowired
    private DataSource dataSource;
    // rememberMe配置
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 赋值数据源
        jdbcTokenRepository.setDataSource(dataSource);
        // 自动创建所需要的表,第一次执行会创建,以后要执行就要删除掉!
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                    .loginPage("/login.html") // 自定义登录页面
                    .loginProcessingUrl("/user/login") // 登录请求url
                    .defaultSuccessUrl("/success.html") // 登录成功后跳转url 同successForwardUrl方法
                    // .failureUrl("/login.html") // 登录失败后跳转url 实际上不配置默认是回到登录页 同failureForwardUrl方法
                    // .usernameParameter("myUserName") // 指定登录时用户名参数名
                    // .passwordParameter("myPassword") // 指定登录时密码参数名
                    .permitAll()
                // 没有权限时会到默认的403页面,可以自定义403没有权限错误提示页面
                .and().exceptionHandling().accessDeniedPage("/unauth.html")
                // 设置退出url 退出后需要重新登录认证
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll()
                // 配置认证与授权
                .and().authorizeRequests()
                    // 认证
                    .antMatchers("/", "/hello", "/user/login").permitAll() // 不需要认证的路径
                    // 授权
                    // 基于权限访问控制, 只有指定一个权限才可以访问
//                    .antMatchers("/hello/index").hasAuthority("admin")
                    // 基于权限访问控制, 只要有其中任意一个权限就可以访问, 多个权限逗号分隔
//                    .antMatchers("/hello/index").hasAnyAuthority("admin,manager")
                    // 基于角色访问控制, 只有指定的角色才可以访问
//                    .antMatchers("/hello/index").hasRole("teacher")
                    // 基于角色访问控制, 只要有任意一个角色就可以访问, 多个权限逗号分隔
//                    .antMatchers("/hello/index").hasAnyRole("student,teacher")
                .anyRequest().authenticated() // 任何请求的url都需要认证
                // remember me
                .and().rememberMe().tokenRepository(persistentTokenRepository())
                    // 设置remember token有效时间 单位:秒
                    .tokenValiditySeconds(60)
                    .userDetailsService(userDetailsService)
                .and().csrf().disable(); // 关闭csrf防护
    }

}

自定义userDetailsService查询数据库

@Component("userDetailsService")
public class JdbcUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    /**
    * 根据用户名查询数据库
    */
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        // 这里采用Mybatis-plus去查询
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName, userName);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 如果是基于权限进行访问控制,需要给用户赋予权限列表名,这里设置的是名为admin的权限
        // 如果是基于角色进行访问控制 角色名固定为ROLE_xxx 可以查看SpringSecurity hasRole()源码 会自动给hasRole()方法设置的角色加上ROLE_前缀
        // 如果设置角色后还是403 可以尝试更换角色名 比如我设置ROLE_abc角色,hasRole("abc")就无法成功
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_teacher");
        return new org.springframework.security.core.userdetails.User(user.getUserName(), new BCryptPasswordEncoder().encode(user.getPassWord()), auths);
    }
}

页面登录认证

页面提交方式必须为 post 请求,用户名,密码输入框的name值必须为 username, password 原因: 在执行登录的时候会走一个过滤器 UsernamePasswordAuthenticationFilter,在这个Filter中定义了默认的name值以及默认只接受post请求

如果需要用户名,密码输入框的name值,可以在configure配置方法中通过 usernameParameter()和 passwordParameter()方法修改

基于角色或权限进行访问控制

这里涉及到四个方法用于基于角色或权限进行访问控制,每种方式各两个方法,在基于注解的url访问控制中使用的也是这四个方法

基于权限

  • hasAuthority 方法:设置用户需要有哪一个权限才能访问,如果当前的主体具有指定的权限就允许访问,否则转发到 403页面
  • hasAnyAuthority 方法:如果当前的主体有任何提供的权限列表(给定一个逗号分隔的字符串列表)的话就允许访问,否则转发到 403页面

基于角色

  • hasRole 方法 如果用户具备给定角色就允许访问,否则转发到 403页面
  • hasAnyRole 表示用户具备任何一个角色都可以访问,否则转发到 403页面

注意:在configure配置方法中不需要ROLE_前缀,但是在自定义的userDetailsService中需要加上ROLE_前缀,这是Spring Security源码中定义的,所以要么在数据库角色表中定义的角色名就加上前缀,要么在自定义的userDetailsService中通过代码处理下给加上前缀

基于注解的访问控制

使用Spring Security提供的注解就需要先开启配置

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
  • @Secured: 判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“

  • @PreAuthorize:注解适合进入方法前的权限验证,只有给定的权限才能访问,在这个注解中使用访问控制中的四个方法

  • @PostAuthorize: 注解在方法执行后再进行权限验证,适合验证带有返回值的方法进行访问控制,在这个注解中使用访问控制中的四个方法

  • @PreFilter: 进入控制器之前对数据进行过滤,此时filterObject表示的是方法入参List中元素id%2==0的参数才能传入方法

  • @PostFilter:权限验证之后对数据进行过滤,此时表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素

更多在注解中可使用的表达式方法可以查看官方文档:https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#el-common-built-in

基于数据库的记住我

  1. 因为是基于数据库的操作,所以关于数据库的配置就不再赘述,需要先生成一张表,可以通过setCreateTableOnStartup(true)方法自动生成,也可以手动创建这张表,这Spring Security定义的表,可以从源码中找到表的DDL
CREATE TABLE `persistent_logins` (
 `username` varchar(64) NOT NULL,
 `series` varchar(64) NOT NULL,
 `token` varchar(64) NOT NULL,
 `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP,
 PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. 在配置类中加入如下代码
// 注入数据源用于rememberMe自动创建表以及将记住的用户保存在数据在
    @Autowired
    private DataSource dataSource;
    // rememberMe配置
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 赋值数据源
        jdbcTokenRepository.setDataSource(dataSource);
        // 自动创建所需要的表,第一次执行会创建,以后要执行就要删除掉!
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
  1. 修改安全配置方法configure
// 开启记住我功能
http.rememberMe()
 .tokenRepository(tokenRepository)
 .userDetailsService(usersService);

  1. 页面定义一个复选框用于选中记住密码,注意:name 属性值必须是 remember-me,不能改为其他值

  2. 设置有效期,默认 2 周时间。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登录,还是修改安全配置方法configure

// 设置remember token有效时间 单位:秒
.tokenValiditySeconds(60)

用户登出注销

用户在页面上通过一个url进行登出注销,在后端Spring Security中需要配置下

http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll

CSRF 跨站请求伪造

CSRF是一种挟制用户在当前已 登录的 Web 应用程序上执行非本意的操作的攻击方法。简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个 自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买 商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。 这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的 浏览器,却不能保证请求本身是用户自愿发出的

从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用 程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护

如果开启CSRF保护,就需要在每个表单请求中携带一个名为_csrf的隐藏域

<input type="hidden"th:if="${_csrf}!=null"th:value="${_csrf.token}"name="_csrf"/>

Spring Security 实现 CSRF是将自动生成的 csrfToken 保存到 HttpSession 或者 Cookie 中,具体实现可以参看HttpSessionCsrfTokenRepository,CookieCsrfTokenRepository类,当请求到来时,从请求中提取 csrfToken,和保存在Session或Cookie中的 csrfToken 做比较,进而判断当前请求是否合法。这个判断过程主要通过 CsrfFilter 过滤器来完成

SpringSecurity 前后端分离权限方案

在前后端分离结构中,后端基本上不再使用Session保存用户状态,而是用使用Token实现用户的无状态,同时保证接口的通用性

根据用户信息使用jwt生成Token并响应给前端,前端请求在请求头中携带Token,在Spring Security中获取请求头中的Token并解析出用户信息,根据用户信息从关系型数据库或nosql中获取权限列表,并由Spring Security给当前用户赋权(认证与授权

在每个接口上可以使用注解对请求进行细粒度的访问控制

核心配置类

核心配置类就是继承 WebSecurityConfigurerAdapter 并注解 @EnableWebSecurity 的配置。这个配置指明了用户名密码的处理方式、请求路径、登录/登出控制等和安全相关的配置

package com.lynu.security.config;

import com.lynu.security.filter.TokenAuthFilter;
import com.lynu.security.filter.TokenLoginFilter;
import com.lynu.security.security.DefaultPasswordEncoder;
import com.lynu.security.security.TokenLogoutHandler;
import com.lynu.security.security.TokenManager;
import com.lynu.security.security.UnauthorizedEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

    // Token管理工具类(使用JWT生成Token, 或者从Token中获取用户信息)
    @Autowired
    private TokenManager tokenManager;
    // redis操作工具类 这里把用户与其对应的权限列表保存在redis,也可以直接从关系型数据库中获取
    @Autowired
    private RedisTemplate redisTemplate;
    // 密码管理工具类 这里使用MD5进行密码加解密 也可以使用Spring security推荐的BCryptPasswordEncoder
    @Autowired
    private DefaultPasswordEncoder passwordEncoder;
    // 自定义查询数据库用户名密码和权限信息
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthorizedEntryPoint()) // 认证失败处理类
                .and().csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated() // 所有请求都需要授权
                .and().logout().logoutUrl("/admin/security/index/logout").permitAll() // 登出处理url
                .addLogoutHandler(new TokenLogoutHandler(redisTemplate, tokenManager)).and() // 登出处理类
                // 配置认证filter
                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
                // 配置授权filter
                .addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate))
                .httpBasic();
    }

    // 处理userDetail(自定义 查询数据库登录和权限列表)和密码处理类
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }

    // 无需认证的url, 可以直接访问
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/api/**", "/swagger-ui.html/**");
    }
}

认证授权相关的工具类

DefaultPasswordEncoder(密码管理工具类)

package com.lynu.security.security;

import com.lynu.util.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return MD5.encrypt(charSequence.toString());
    }

    @Override
    public boolean matches(CharSequence charSequence, String password) {
        return password.equals(MD5.encrypt(charSequence.toString()));
    }
}

TokenManager(Token管理工具类)

package com.lynu.security.security;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class TokenManager {

    // Token有效时长
    private static final long expirTime = 24 * 60 * 60 * 1000;
    // Token密钥
    private static final String tokenSignKey = "123456";

    /**
     * 根据用户名生成Token
     * @param userName 用户名
     * @return
     */
    public String createToken(String userName) {
        return Jwts.builder()
                .setSubject(userName)
                .setExpiration(new Date(System.currentTimeMillis() + expirTime))
                .signWith(SignatureAlgorithm.HS256, tokenSignKey)
                .compact();
    }

    /**
     * 从Token中解析出用户名
     * @param token Token字符串
     * @return
     */
    public String getUserNameFromToken(String token) {
        return Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJwt(token).getBody().getSubject();
    }

    /**
     * 移除Token
     * @param token 移除Token
     */
    public void removeToken(String token) {
		// 这里使用空方法模拟移除,也可以编写代码让Token失效
    }

}

TokenLogoutHandler(登出处理类)

package com.lynu.security.security;

import com.lynu.util.utils.R;
import com.lynu.util.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TokenLogoutHandler implements LogoutHandler {

    private RedisTemplate redisTemplate;
    private TokenManager tokenManager;

    public TokenLogoutHandler(RedisTemplate redisTemplate, TokenManager tokenManager) {
        this.redisTemplate = redisTemplate;
        this.tokenManager = tokenManager;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // 从Header中获取Token
        String token = request.getHeader("Token");
        if (token != null) {
            // 移除Token
            tokenManager.removeToken(token);
            // 从Redis中删除Token
            redisTemplate.delete("");
        }
        // 通过响应流输出
        ResponseUtil.out(response, R.ok());
    }
}

UnauthorizedEntryPoint(未授权统一处理类)

package com.lynu.security.security;

import com.lynu.util.utils.R;
import com.lynu.util.utils.ResponseUtil;
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;

public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}

ResoinseUtil(响应工具类)

package com.lynu.util.utils;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ResponseUtil {

    public static void out(HttpServletResponse response, R r) {
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            mapper.writeValue(response.getWriter(), r);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

创建认证授权实体类

SecurityUser(安全实体类)

实现了UserDetails接口,同时包含普通用户实体User

package com.lynu.security.entity;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Data
@Slf4j
public class SecurityUser implements UserDetails {
    // 当前登录用户
    private transient User currentUserInfo;
    // 当前用户所有的权限列表字符串集合
    private List<String> permissionValueList;
    
    public SecurityUser() {
    }
    
    public SecurityUser(User user) {
        if (user != null) {
            this.currentUserInfo = user;
        }
    }

    /**
     * 将当前用户的权限列表封装为Collection<? extends GrantedAuthority>类型
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for(String permissionValue : permissionValueList) {
            if(StringUtils.isEmpty(permissionValue)) continue;
            SimpleGrantedAuthority authority = new
                    SimpleGrantedAuthority(permissionValue);
            authorities.add(authority);
        }
        return authorities;
    }
    
    @Override
    public String getPassword() {
        return currentUserInfo.getPassword();
    }
    @Override
    public String getUsername() {
        return currentUserInfo.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;
    }

}

User(普通用户实体类)

User对应业务相关,数据表相关的用户实体,需按实际情况修改该类的字段

package com.lynu.security.entity;

import io.swagger.annotations.ApiModel;
import lombok.Data;

import java.io.Serializable;

@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {
    private String username;
    private String password;
    private String nickName;
    private String salt;
    private String token;
}

创建认证和授权的 filter

TokenLoginFilter(认证的filter)

package com.lynu.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lynu.security.entity.SecurityUser;
import com.lynu.security.entity.User;
import com.lynu.security.security.TokenManager;
import com.lynu.util.utils.R;
import com.lynu.util.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    private AuthenticationManager authenticationManager;

    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.authenticationManager = authenticationManager;
        // 只有POST请求才认证设置为false 认证所有请求方式
        this.setPostOnly(false);
        // 认证的请求url和请求方式
        this.setRequiresAuthenticationRequestMatcher(new
                AntPathRequestMatcher("/admin/security/login","POST"));
    }

    /**
     * 获取认证提交的用户名和密码
     * 该方法首先被调用 调用之后会再调用UserDetail的loadUserByUsername()方法
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 获取提交的数据转换为User对象
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            // 用户名 密码 权限列表(这里先给一个空权限列表)
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),
                    user.getPassword(), new ArrayList<>()));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * 认证成功后调用的方法
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityUser user = (SecurityUser) authResult.getPrincipal();
        // 把k:用户名 v:权限列表放入redis
        redisTemplate.opsForValue().set(user.getUsername(), user.getPermissionValueList());
        // 根据user生成Token
        String token = tokenManager.createToken(user.getUsername());
        // 返回Token
        ResponseUtil.out(response, R.ok().data("token", token));
    }

    /**
     * 认证失败后调用的方法
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        // 响应失败信息
        ResponseUtil.out(response, R.error());
    }
}

TokenAuthFilter(授权的filter)

package com.lynu.security.filter;

import com.lynu.security.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class TokenAuthFilter extends BasicAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenAuthFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        super(authenticationManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 获取当前认证成功用户的权限
        UsernamePasswordAuthenticationToken authToken = getAuthentication(request);
        // 如果权限不为空将权限放入Spring Security上下文
        if (authToken != null) {
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
    }

    // 根据请求头获取用户名 根据用户名获取权限
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader("token");
        if (token != null) {
            String userName = tokenManager.getUserNameFromToken(token);
            // 从redis中获取权限列表
            List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            for (String permission : permissionValueList) {
                authorities.add(new SimpleGrantedAuthority(permission));
            }
            // 用户名 token 权限列表
            return new UsernamePasswordAuthenticationToken(userName, token, authorities);
        }
        return null;
    }

}

UserDetailServiceImpl(自定义查询数据的UserDetailsService)

package com.lynu.securityservice.config;

import com.lynu.securityservice.entity.User;
import com.lynu.securityservice.service.PermissionService;
import com.lynu.securityservice.service.UserService;
import com.lynu.security.entity.SecurityUser;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.List;

@Component("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;
    @Autowired
    private PermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        // 根据用户名查询数据库
        User user = userService.selectByUsername(userName);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // 这里是因为DO对象是com.lynu.securityservice.entity.User, 而SecurityUser中对象是com.lynu.security.entity.User, 所以需要copy,如果两个对象一致可以省略这步骤
        com.lynu.security.entity.User curUser = new com.lynu.security.entity.User();
        BeanUtils.copyProperties(user, curUser);
        // 根据用户Id查询权限
        List<String> permissionList = permissionService.selectPermissionValueByUserId(user.getId());
        // 根据当前用户和权限列表构建UserDetails
        SecurityUser securityUser = new SecurityUser(curUser);
        securityUser.setPermissionValueList(permissionList);
        return securityUser;
    }
}
posted @ 2021-03-16 20:39  OverZeal  阅读(627)  评论(0编辑  收藏  举报