Spring Security(四)基于redis的分布式认证鉴权解决方案

项目介绍

方案介绍

​ 基于数据库的认证鉴权方式,登录之后会产生一个session存储在内存之中,然后将sessionId返回给客户端,后续客户端请求资源时,会将sessionId携带上,服务端根据sessionId判断用户的登录状态,如果用户登录过,则放行,如果没有登录,则会被拦截,跳转到登录页面重新登录,但这种将登录状态以及信息放在内存中的方式会带来两个问题:

  1. 由于session产生后放在服务器的内存之中,服务器因为某种原因(宕机或者更新)重启之后,则所有的session都会丢失,那么登录过的所有用户都需要重新登录;
  2. 分布式场景下,可能登录是在服务器A上,那么session保存在服务器A上,下一个请求,可能由服务器B处理,那么服务器B上没有这个session,就需要重新登录;

通过将session保存到redis中实现session共享解决分布式场景下的以上两个问题,

Browser Server Redis 使用用户名、密码登录 验证用户名密码,并产生SecuriryContext 将SecuriryContext success sessionId 携带sessionId请求资源 根据sessionId查询SecuriryContext SecuriryContext 处理请求 返回相应 Browser Server Redis

项目结构

项目源码:https://github.com/xdouya/Spring-Security-demo/tree/master/04-mybatis-redis-security
Spring Security(四)基于redis的分布式认证鉴权解决方案_第1张图片

项目构建

数据表创建以及用户数据导入

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `username` varchar(50) NOT NULL,
  `password` varchar(500) NOT NULL,
  `enabled` tinyint(1) NOT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB;

INSERT  IGNORE INTO `users` VALUES ('admin','{bcrypt}$2a$10$SAqQq0WEQYRA4etZpRa6e.Kew0sKKtC/ahFrSZXS1iHsy5EhZqLsa',1),('user','{bcrypt}$2a$10$SAqQq0WEQYRA4etZpRa6e.Kew0sKKtC/ahFrSZXS1iHsy5EhZqLsa',1),('vip','{bcrypt}$2a$10$SAqQq0WEQYRA4etZpRa6e.Kew0sKKtC/ahFrSZXS1iHsy5EhZqLsa',1);

DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
  `username` varchar(50) NOT NULL,
  `authority` varchar(50) NOT NULL,
  UNIQUE KEY `ix_auth_username` (`username`,`authority`),
  CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`)
) ENGINE=InnoDB;

INSERT  IGNORE INTO `authorities` VALUES ('admin','ROLE_admin'),('user','ROLE_user'),('vip','ROLE_vip');

这里的这个users表和authorities表的字段没有强制要求,只要后续查询的时候能对应上就可以了;

Maven依赖

  • pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.sessiongroupId>
        <artifactId>spring-session-data-redisartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-securityartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
        <version>2.1.3version>
    dependency>
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druid-spring-boot-starterartifactId>
        <version>1.1.10version>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-devtoolsartifactId>
        <scope>runtimescope>
        <optional>trueoptional>
    dependency>
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <scope>runtimescope>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <optional>trueoptional>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintagegroupId>
                <artifactId>junit-vintage-engineartifactId>
            exclusion>
        exclusions>
    dependency>
    <dependency>
        <groupId>io.projectreactorgroupId>
        <artifactId>reactor-testartifactId>
        <scope>testscope>
    dependency>
    <dependency>
        <groupId>org.springframework.securitygroupId>
        <artifactId>spring-security-testartifactId>
        <scope>testscope>
    dependency>
dependencies>

Spring Security配置

  • Srping Security配置
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 配置拦截器保护请求
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/vip/**").hasRole("vip")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated()
                .and().formLogin()
                .and().httpBasic();
    }

    /**
     * 根据自动匹配密码编码器
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}
  • Redis配置
/**
 * @author caiwl
 * @date 2020/8/21 16:58
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(genericJackson2JsonRedisSerializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
  • spring-session配置
@Configurable
@EnableRedisHttpSession
public class SessionConfig {
}
  • MyUserDetailServiceImpl,通过实现UserDetailsService#loadUserByUsername方法自定用户信息查询
/**
 * 自定义UserDetailsService
 * @author caiwl
 * @date 2020/8/20 17:06
 */
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {

    private UserDao userDao;

    @Autowired
    public MyUserDetailServiceImpl(UserDao userDao){
        this.userDao = userDao;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("整合mybatis 查询用户信息");
        return userDao.loadUserByUsername(username);
    }
}
  • UserDao,用户数据访问接口
/**
 * @author caiwl
 * @date 2020/8/20 17:09
 */
public interface UserDao {
    /**
     * 根据用户名查询用户信息
     * @param username 用户名
     * @return 用户信息
     */
    UserPo loadUserByUsername(String username);
}
  • UserMapper.xml, mapper文件



<mapper namespace="org.dy.security.dao.UserDao">
	<resultMap type="org.dy.security.entiy.UserPo" id="UserMap">
		<id column="username" property="username"/>
		<result column="password" property="password"/>	
		<collection property="authorities" ofType="org.dy.security.entiy.RolePo">
			<id column="username" property="username"/>
			<result column="authority" property="authority"/>
		collection>		
	resultMap>

	<select id="loadUserByUsername" resultMap="UserMap">
		select 
			users.username, users.password, authorities.authority
		from 
			users left join authorities on users.username = authorities.username
		 where users.username=#{username}
	select>
mapper>
  • UserPo,用户信息
/**
 * @author caiwl
 * @date 2020/8/20 17:08
 */
@Data
public class UserPo implements UserDetails, Serializable {

    private static final long serialVersionUID = 1L;

    private String username;
    private String password;
    private List<RolePo> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        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 true;
    }

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • RolePo,角色信息
/**
 * @author caiwl
 * @date 2020/8/20 17:09
 */
@Data
public class RolePo implements GrantedAuthority {

    private static final long serialVersionUID = 1L;
    private String username;
    private String authority;

    @Override
    public String getAuthority() {
        return authority;
    }
}
  • HelloController,控制器接口
/**
 * @author caiwl
 * @date 2020/8/9 14:05
 */
@RestController
public class HelloController {

    @GetMapping("/")
    public String hello(){
        return "hello, welcome";
    }

    @GetMapping("/admin/hello")
    public String helloAdmin(){
        return "hello admin";
    }

    @GetMapping("/vip/hello")
    public String helloVip(){
        return "hello vip";
    }

    @GetMapping("/user/hello")
    public String helloUser(){
        return "hello user";
    }
}

项目测试

访问http://localhost:8080/,输入用户名,密码admin:088114访问,可以发现session保存在redis里面了;
Spring Security(四)基于redis的分布式认证鉴权解决方案_第2张图片

思考

​ 以上介绍的都是基于session来记录用户的登录状态,这种基于session的方式有如下几个问题

  1. 每一个用户都会生成一个session,当用户量巨大的时候,巨量的session会占用巨大的服务端内存
  2. 可以通过将session缓存在redis中共享,解决分布式场景下的登录状态记录,但是一旦redis宕机后,会造成所有用户都无法访问任何需要认证的资源;

下一节,介绍Spring Security(五)基于JWT令牌的认证鉴权的这种无状态的解决方法来规避以上两个问题

你可能感兴趣的:(Spring,Security,分布式,java,mysql,session,redis)