前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Security入门(三): 基于自定义数据库查询的认证实战

Spring Security入门(三): 基于自定义数据库查询的认证实战

作者头像
用户3587585
发布2022-09-21 08:37:58
1.3K0
发布2022-09-21 08:37:58
举报
文章被收录于专栏:阿福谈Web编程阿福谈Web编程

0 引言

在笔者的上一篇文章中Spring Security入门(二):基于内存的认证一文中有提到过Spring Security实现自定义数据库查询需要你实现UserDetailsService接口,并实现loadUserByUsername(String username)抽象方法。我们可以在UserDetailsService接口的实现类中注入数据库访问对象Dao,从而实现自定义数据库查询认证用户信息。下面在笔者的boot-demo实战项目中我们结合spring data jpa作为持久层技术来一步一步实现自定义数据库认证。

1 表结构设计与实体类

1.1 新建用户表tbl_user与对应实体类

笔者使用的数据库为mysql5.6, 在IDEA中新建一个客户端连接,并在就控制台窗口中执行如下新建tbl_user表的脚本:

代码语言:javascript
复制
use mysql;
create table tbl_user(
  user_id numeric(8) primary key,
  username varchar(30) not null comment '用户名',
  username_zh varchar(50) not null comment '用户中文名',
  password varchar(100) not null comment '密码',
  enabled bool default true,
  locked bool default false
)engine=InnoDB default CHARSET=utf8;

create unique index user_name_index on tbl_user(username);

然后执行插入两条数据:

代码语言:javascript
复制
#张三原始密码为zhangsan123
insert into tbl_user(user_id, username, username_zh, password)
values(1,'x_zhangsan','张三','$2a$10$aKLAkRgoEImFQ2g14W.mjO.Y66JKKHLlXycrDd9G9uJ54uQsxjPCO');

#李四原始密码为lisi123
insert into tbl_user(user_id, username, username_zh, password)
values(2,'x_lisi','李四','$2a$10$HgIzJr..ahIj96fF4ljpEe8UsN5LYJ8pmggxP9dfaZZWb8JqCoQ/G');

commit ;

为了维护用户敏感信息的安全,数据库里用户的登录密码或支付密码等安全性要求较高的字段一律采用加密存储的方式存储。

添加用户的sql脚本中用户的加密密文均在是IDEA中的命令控制台执行spring-boot-cli命令spring encodepassword ${password}的方式获得,其实质是使用BCryptPasswordEncoder编码原始密码所得

tbl_user表的建表依据为:在spring security自定义用户类必须实现UserDetailsUserDetails的源码如下:

代码语言:javascript
复制
public interface UserDetails extends Serializable {
    //获取权限列表
    Collection<? extends GrantedAuthority> getAuthorities();
    //获取密码
    String getPassword();
    //获取用户名
    String getUsername();
    //判断用户的账号是否没有过期:返回 true为没过期;返回false为过期
    boolean isAccountNonExpired();
    //判断用户账号是否没有被锁住:返回true为账号没被锁住;返回false为账号被锁住
    boolean isAccountNonLocked();
    //判断凭据是否没有过期:返回true为未过期;返回false为已过期
    boolean isCredentialsNonExpired();
    //判断账号是否启用:返回true为启用;返回false为禁用
    boolean isEnabled();
}

于是我们创建一个实现UserDetails接口的实现类并使之与tbl_user表中的字段一一对应

user.java

代码语言:javascript
复制
Entity(name="tbl_user")
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="user_id",nullable = false,length = 8)
    private Long userId;

    @Column(name="username",nullable = false,length = 30)
    private String username;

    @Column(nullable = false,length = 50)
    private String usernameZh;

    @Column(nullable = false,length = 100)
    private String password;

    //1: 启用;0:禁用
    @Column(nullable = true,length = 1)
    private Integer enabled;

    //1:锁住;0:未锁
    @Column(nullable = true,length = 1)
    private Integer locked;

    @Transient
    private List<Role> roles;

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setUsernameZh(String usernameZh) {
        this.usernameZh = usernameZh;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setEnabled(Integer enabled) {
        this.enabled = enabled;
    }

    public void setLocked(Integer locked) {
        this.locked = locked;
    }

    public Long getUserId() {
        return userId;
    }

    public String getUsernameZh() {
        return usernameZh;
    }

    public Integer getEnabled() {
        return enabled;
    }

    public Integer getLocked() {
        return locked;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for(Role role: roles){
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleId());
            authorities.add(authority);
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

    @Override
    public boolean isAccountNonLocked() {
        return locked==0;
    }

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

    @Override
    public boolean isEnabled() {
        return enabled==1;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
    //重写equals和hashCode方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(userId, user.userId) &&
                Objects.equals(username, user.username);
    }

    @Override
    public int hashCode() {
        return Objects.hash(userId, username);
    }
}

1.2 新建角色表roels及其对应的实体类

roles表的建表sql脚本如下:

代码语言:javascript
复制
use mysql;
create table roles(
  role_id varchar(30) comment '角色ID',
  role_name varchar(50) not null comment '角色名',
  created_by varchar(50) default 'system',
  created_time datetime default now(),
  primary key (role_id)
)engine=InnoDB default CHARSET=utf8;

执行难添加用户sql脚本:

代码语言:javascript
复制
insert into roles(role_id, role_name,created_by)
values('Admin','管理员','x_heshengfu');

insert into roles(role_id, role_name,created_by)
values('SystemAdmin','系统管理员','x_heshengfu');

insert into roles(role_id, role_name,created_by)
values('Developer','开发人员','x_heshengfu');

insert into roles(role_id, role_name,created_by)
values('Guest','普通客户','x_heshengfu');

commit;

新建roles表对应是实体类Role.java:

代码语言:javascript
复制
@Entity(name="roles")
public class Role implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="role_id",length = 30,nullable = false)
    private String roleId;

    @Column(name="role_name",length = 50,nullable = false)
    private String roleName;

    @Column(name="created_by",nullable = true,length = 50)
    private String createdBy;

    @Column(name="created_time",nullable = true)
    private String createdTime;

   //......此处省略setter和getter方法
}

1.3 新建用户-角色关系关系表tbl_user_role及其对应的实体类

tbl_user_role表的建表sql脚本如下:

代码语言:javascript
复制
use mysql;
create table tbl_user_role(

  user_role_id numeric(10) primary key COMMENT '主键',

  user_id numeric(8) not null comment '用户id',

  role_id varchar(30) not null comment '角色id'
)engine=InnoDB default CHARSET=utf8;

create unique index user_role_id_index on tbl_user_role(user_id,role_id);

执行往tbl_user_role表添加数据sql脚本:

代码语言:javascript
复制
--插入数据
insert into tbl_user_role(user_role_id, user_id, role_id)
values(1,1,'Admin');

insert into tbl_user_role(user_role_id, user_id, role_id)
values(2,1,'SystemAdmin');

insert into tbl_user_role(user_role_id, user_id, role_id)
values(3,2,'Developer');

insert into tbl_user_role(user_role_id, user_id, role_id)
values(4,2,'Guest');

commit;

新建tbl_user_role表对应的实体类UserRole.java

代码语言:javascript
复制
@Entity(name="tbl_user_role")
public class UserRole implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="user_role_id")
    private Long userRoleId;

    @Column(name="role_id",nullable=false, length = 30)
    private String roleId;

    @Column(name="user_id",nullable = false, length = 8)
    private Long userId;

     //......此处省略setter和getter方法
}

2 用于认证用户信息的数据库访问层

2.1 新建与用户表对应的Repository接口

代码语言:javascript
复制
public interface TblUserRepository extends JpaRepository<User,Long> {

    User findUserByUsername(String username);

}

TblUserRepository接口继承JpaRepository接口,自动拥有了基本的CRUD、分页查询方法及根据字段和关键字查找表对应实体类信息的功能。在TblUserRepository接口中我们自定义了一个根据username字段查找用户信息的方法,继承自JpaRepository接口的数据库访问接口无需开发人员手动实现其中 2.2 新建与角色表对应的Repository接口

代码语言:javascript
复制
public interface RoleRepository extends JpaRepository<Role,String> {

    List<Role> findRolesByRoleIdIn(List<String> roleIds);
}

RoleRepository接口中笔者自定义了根据角色id列表查询角色列表的抽象方法,方便给用户查询角色列表

2.3 新建与用户角色关系表对应的Repository接口

代码语言:javascript
复制
public interface UserRoleRepository extends JpaRepository<UserRole,Long> {

   List<UserRole> findByUserId(Long userId);

}

UserRoleRepository接口中,笔者定义了根据角色id查询用户角色列表的抽象方法。

3 定义UserDetailsService接口的实现类

代码语言:javascript
复制
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private TblUserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private UserRoleRepository userRoleRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findUserByUsername(username);
        if(user==null){
            return null;
        }
        Long userId = user.getUserId();
        List<UserRole> userRoleList = userRoleRepository.findByUserId(userId);
        if(userRoleList.size()>0){
            List<String> roleIds = new ArrayList<>();
            for(UserRole userRole: userRoleList){
                roleIds.add(userRole.getRoleId());
            }
            List<Role> roles = roleRepository.findRolesByRoleIdIn(roleIds);
            user.setRoles(roles);
        }
        return user;
    }
}

以上为了从数据库中查出登录用户的用户名、加密密文及角色列表从数据库中查了3次。

  • 第1次通过TblUserRepository#findUserByUsername传入username参数查出不包含角色信息的User对象,如果用户不存在则直接返回null;
  • 第2次通过UserRoleRepository#findByUserId 传入用户id查出用户-角色关系列表
  • 第3步通过第2部中得到了角色id列表作为入参传入到RoleRepository#findRolesByRoleIdIn方法得到完整的角色信息列表

由于使用spring-data-jpa 实现关联查询笔者暂时还没有掌握,因而以上认证用户信息访问了三次数据库,确实容易影响效率;在实际的商用生产环境可以参照spring-data-jpa的连接查询改为连接查询,对于用户登录认证信息等热点数据首次你从数据库查询出来后最好缓存在redis缓存中,并设置过期时间。另外如果是使用mybatis作为数据库持久层框架,可以借助resultMap集合association属性通过一条sql将包含角色列表的用户信息一次性查出来

4 WebSecurityConfigurerAdapter实现类中配置userDetailsService

4.1 配置userDetailsService

代码语言:javascript
复制
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailServiceImpl userDetailService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());

    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests().antMatchers("/index/user").hasAnyRole("Admin","Developer","SystemAdmin")
                .antMatchers("/index/admin").hasAnyRole("Admin","SystemAdmin")
                .anyRequest().authenticated()
                .antMatchers("/login").permitAll()
                .and().formLogin().loginProcessingUrl("/login").
                usernameParameter("username").passwordParameter("password")
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
                        Object principal = auth.getPrincipal();
                        response.setContentType("application/json;charset=utf-8");
                        response.setStatus(200);
                        PrintWriter writer = response.getWriter();
                        Map<String,Object> map = new HashMap<>();
                        map.put("status",200);
                        map.put("msg","login success");
                        map.put("data",principal);
                        ObjectMapper objectMapper = new ObjectMapper();
                        writer.write(objectMapper.writeValueAsString(map));
                        writer.flush();
                        writer.close();
                    }
                }).failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws IOException, ServletException {
                response.setContentType("application/json ;charset=utf-8");
                PrintWriter writer = response.getWriter();
                response.setStatus(401);
                Map<String,Object> map = new HashMap<>();
                map.put("status",401);
                if(ex instanceof LockedException){
                    map.put("msg","账号被锁定,登录失败");
                }else if(ex instanceof BadCredentialsException){
                    map.put("msg","账号或密码输入错误,登录失败");
                }else if(ex instanceof DisabledException){
                    map.put("msg","账户被禁用,登录失败");
                }else if(ex instanceof CredentialsExpiredException){
                    map.put("msg","密码过期,登录失败");
                }else{
                    map.put("msg","登录失败");
                }
                ObjectMapper objectMapper = new ObjectMapper();
                writer.write(objectMapper.writeValueAsString(map));
                writer.flush();
                writer.close();
            }
        })
                .permitAll()
                 .and();
    
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return  new BCryptPasswordEncoder();
    }
}

在上文《Spring Security入门(二) 基于内存存储的表单登录实战》的基础上对所有用户进入登录页面和登录接口放开权限,而对/index/*路径下的接口允许访问角色改为数据库中存在的Admin,SystemAdmin,Developer等角色。登录成功处理器和失败处理器配置用沿用上文中的逻辑。

4.2 测试用户登录认证效果

在浏览器中输入 http://localhost:8088/apiBoot/login 回车即可进入登录页面

右键->检查 在下方在弹出的元素审查窗口中选中Elements标签查看表单的html源码,我们可以看到登录表单中实际还包含了一个隐藏了_csrf输入框,其值为622251f2-f7f3-4b78-88a0-52451771deaf,是一个UUID字符串,它的用处是为了保护web请求,防止跨站请求伪造(简称CSRF)

输入数据库中存在的用户账号x_zhangsan己密码zhangsan123,点击Sign in即可登录成功,登录成功后浏览器中会返回如下信息:

代码语言:javascript
复制
"msg":"login success","data":{"userId":1,"username":"x_zhangsan","usernameZh":"张三","password":"$2a$10$aKLAkRgoEImFQ2g14W.mjO.Y66JKKHLlXycrDd9G9uJ54uQsxjPCO","enabled":1,"locked":0,"roles":[{"roleId":"Admin","roleName":"管理员","createdBy":"x_heshengfu","createdTime":"2020-04-19 23:07:34"},{"roleId":"SystemAdmin","roleName":"系统管理员","createdBy":"x_heshengfu","createdTime":"2020-04-19 23:07:34"}],"authorities":[{"authority":"Admin"},{"authority":"SystemAdmin"}],"credentialsNonExpired":true,"accountNonLocked":true,"accountNonExpired":true},"status":200}

登录成功后的返回信息中包含了用户的基本属性和角色及权限信息。

使用postman登录需要带上_csrftoken值:

代码语言:javascript
复制
POST http://localhost:8088/apiBoot/login?username=x_zhangsan&password=zhangsan123&_csrf=66dff592-bc63-488f-ab34-929258a55db6
//响应信息如下:
{
    "msg": "login success",
    "data": {
        "userId": 1,
        "username": "x_zhangsan",
        "usernameZh": "张三",
        "password": "$2a$10$aKLAkRgoEImFQ2g14W.mjO.Y66JKKHLlXycrDd9G9uJ54uQsxjPCO",
        "enabled": 1,
        "locked": 0,
        "roles": [
            {
                "roleId": "Admin",
                "roleName": "管理员",
                "createdBy": "x_heshengfu",
                "createdTime": "2020-04-19 23:07:34"
            },
            {
                "roleId": "SystemAdmin",
                "roleName": "系统管理员",
                "createdBy": "x_heshengfu",
                "createdTime": "2020-04-19 23:07:34"
            }
        ],
        "authorities": [
            {
                "authority": "Admin"
            },
            {
                "authority": "SystemAdmin"
            }
        ],
        "credentialsNonExpired": true,
        "accountNonLocked": true,
        "accountNonExpired": true
    },
    "status": 200
}

在postman 中响应信息得到了json格式的美化,看起来非常清晰

5 存储用户认证信息类的源码解读

5.1 认识SecurityContextHolderSecurityContext

用户登录成功后的认证信息最终能会作为一个Authentication 实现类对象(表单登录通常对应的是一个UsernamePasswordAuthenticationToken对象)对象被认证过滤器保存在SecurityContextHolder类的SecurityContext(安全上下文)中,之后就可以通过SecurityContextHolder这个类直接去获取当前登录用户的认证信息了,SecurityContextHolder其实就是一个存放用户具体认证信息的工具类。

通过查看这两个类的相关源码可以对Spring Security安全框架是如何保存用户的认证信息的原理会有一个更全面的认识,相关源码如下:

SecurityContextHolder.java

代码语言:javascript
复制
public class SecurityContextHolder{
    //静态方法可通过类名直接拿到SecurityContext实例
    public static SecurityContext getContext() {
        return strategy.getContext();
    }
     //设置SecurityContext实例
     public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }
    //创建一个空的 SecurityContext实例
     public static SecurityContext createEmptyContext() {
        return strategy.createEmptyContext();
    }
    //......其他源码被省略
}

SecurityContext.java

代码语言:javascript
复制
public interface SecurityContext extends Serializable {
    //获取认证信息
    Authentication getAuthentication();
    //设置认证信息
    void setAuthentication(Authentication authentication);
}

SecurityContext类是一个接口,它的实现类是SecurityContextImpl

(1)利用SecurityContextHolder保存用户认证信息的示例源码:

代码语言:javascript
复制
//第1步创建一个空的SecurityContext对象实例
SecurityContext context = SecurityContextHolder.createEmptyContext(); 
//第2步根据用户信息、密码和权限构造一个Authentication对象实例
Authentication authentication =
 new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
//第3步将认证信息实例authentication保存到SecurityContext对象实例中
context.setAuthentication(authentication);
//第4步将SecurityContext对象实例保存到SecurityContextHolder类中
SecurityContextHolder.setContext(context);
  • 采用SecurityContextHolder.createEmptyContext()方法,而不是使用SecurityContextHolder.getContext().setAuthentication(authentication) 获取一个SecurityContext对象实例的目的是为了避免多线程场景下的跨线程竞争
  • Spring Security不关心是何种Authentication的实现类实例被设置到SecurityContext实例中,测试场景下使用TestingAuthenticationToken实现类是为了方便,大多数生产场景下一般选用UsernamePasswordAuthenticationToken(userDetails, password, authorities)构造Authentication实现类对象实例
  • 最后SecurityContext实例被保存到SecurityContextHolder类后,Spring Security会使用这些信息来进行后面当前认证用户在每一个限权操作的权限鉴定,简称鉴权(authorization)

(2)利用SecurityContextHolder获取用户的认证信息和权限的代码实例如下:

代码语言:javascript
复制
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities =
authentication.getAuthorities();

默认情况下,SecurityContextHolder使用一个ThreadLocal对象用于存储用户的详细认证信息,这也就意味着即时当前SecurityContext对象没有作为一个参数传递到具体的方法里去,同一个线程中的任意方法都能拿到SecurityContext对象,进而拿到用户的认证信息。如果在当前主体的请求被处理后清除线程程,以这种方式使用ThreadLocal是非常安全的。Spring Security FilterChainProxy(过滤链代理)确保了 SecurityContext永远是干净的。

5.2 认识SecurityContextHolder类中的SecurityContextHolderStrategy

由于一些应用与线程特殊的工作方式,并非所有的应用都完全适合使用ThreadLocal对象来存储安全上下文。例如对于一个Swing客户端应用就要求虚拟机种所有线程共享一个安全上下文对象,这种情况修啊需要选择全局策略。

SecurityContextHolder一共由三种方式存储SecurityContext对象,可以通过在应用启动前调用方法SecurityContextHolder.setStrategyName(String strategyName)方法进行设置。三种策略模式分别是

  • SecurityContextHolder.MODE_GLOBAL: 全局模式,适用于单个应用要求虚拟机中所有线程要求共享一个安全上下文的场景
  • SecurityContextHolder.MODE_INHERITABLETHREADLOCAL:继承本地线程模式
  • SecurityContextHolder.MODE_THREADLOCAL: 本地线程模式

SecurityContextHolder.java

代码语言:javascript
复制
 public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    public static final String SYSTEM_PROPERTY = "spring.security.strategy";
    private static String strategyName = System.getProperty("spring.security.strategy");
    private static SecurityContextHolderStrategy strategy;
    private static int initializeCount = 0;

private static void initialize() {
        if (!StringUtils.hasText(strategyName)) {
            strategyName = "MODE_THREADLOCAL";
        }

        if (strategyName.equals("MODE_THREADLOCAL")) {
            strategy = new ThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals("MODE_GLOBAL")) {
            strategy = new GlobalSecurityContextHolderStrategy();
        } else {
            try {
                Class<?> clazz = Class.forName(strategyName);
                Constructor<?> customStrategy = clazz.getConstructor();
                strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
            } catch (Exception var2) {
                ReflectionUtils.handleReflectionException(var2);
            }
        }

        ++initializeCount;
    }

 static {
        initialize();
 }

通过阅读SecurityContextHolder类中的关键源码,可以看出SecurityContextHolder类首先通过系统变量名spring.security.strategy从系统属性中获取strategyName,并在初始化方法中根据strategyName去实例化strategy属性。在初始化方法中,首先判断strategyName变量是否为空,为空的化就使用MODE_THREADLOCAL模式,然后根据strategyName的值去构建不同的SecurityContextHolderStrategy实现类实例。MODE_THREADLOCAL模式对应ThreadLocalSecurityContextHolderStrategy类实例;MODE_INHERITABLETHREADLOCAL模式对应InheritableThreadLocalSecurityContextHolderStrategy类实例;MODE_GLOBAL对应GlobalSecurityContextHolderStrategy类实例。而SecurityContextHolder类的三个重要的静态方法getContext、setContext和createEmptyContext其实都是委托给strategy来操作的。

通过阅读ThreadLocalSecurityContextHolderStrategy类的源码,我们也可以看到SecurityContext确实是保存在了一个ThreadLocal对象中的泛型变量中。

代码语言:javascript
复制
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();

    ThreadLocalSecurityContextHolderStrategy() {
    }

    public void clearContext() {
        contextHolder.remove();
    }

    public SecurityContext getContext() {
        SecurityContext ctx = (SecurityContext)contextHolder.get();
        if (ctx == null) {
            ctx = this.createEmptyContext();
            contextHolder.set(ctx);
        }

        return ctx;
    }

    public void setContext(SecurityContext context) {
        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        contextHolder.set(context);
    }

    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
}

大多数应用场景下,我们无需改变默认的strategyName的值,默认使用ThreadLocal存储当前登录用户的认证信息即可。

本文代码晚点我会提交到gitee 个人仓库,地址:https://gitee.com/heshengfu1211/boot-demo.git

感兴趣的小伙伴可以克隆下来参考完整的代码

6 参考文章

[1] https://docs.spring.io/spring-security/site/docs/5.4.1/reference/html5/#servlet-authentication

7 推荐阅读

[1] Spring Security 入门(一)Spring Security中的认证与密码编码器

[2] Spring Security入门(二) 基于内存存储的表单登录实战

[3] SpringBoot之路(二)使用用Spring-Data-JPA访问数据库进行基本的CRUD操作

[4] SpringBoot之路(四)Spring-Data-Jpa中的高级应用

读者对本文有任何疑问可在下面的留言板中留言,我看到后会及时回复

---END---

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 阿福谈Web编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档