基于数据库的认证鉴权方式,登录之后会产生一个session存储在内存之中,然后将sessionId返回给客户端,后续客户端请求资源时,会将sessionId携带上,服务端根据sessionId判断用户的登录状态,如果用户登录过,则放行,如果没有登录,则会被拦截,跳转到登录页面重新登录,但这种将登录状态以及信息放在内存中的方式会带来两个问题:
通过将session保存到redis中实现session共享解决分布式场景下的以上两个问题,
项目源码:https://github.com/xdouya/Spring-Security-demo/tree/master/04-mybatis-redis-security
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表的字段没有强制要求,只要后续查询的时候能对应上就可以了;
<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>
@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();
}
}
/**
* @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;
}
}
@Configurable
@EnableRedisHttpSession
public class SessionConfig {
}
/**
* 自定义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);
}
}
/**
* @author caiwl
* @date 2020/8/20 17:09
*/
public interface UserDao {
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return 用户信息
*/
UserPo loadUserByUsername(String username);
}
<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>
/**
* @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;
}
}
/**
* @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;
}
}
/**
* @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里面了;
以上介绍的都是基于session来记录用户的登录状态,这种基于session的方式有如下几个问题
下一节,介绍Spring Security(五)基于JWT令牌的认证鉴权的这种无状态的解决方法来规避以上两个问题