likes
comments
collection
share

深入浅出SpringSecurity--认证篇

作者站长头像
站长
· 阅读数 11

SpringSecurity简介

SpringSecurity作为Spring家族中的一员,提供了一套 Web 应用安全性的完整解决方

案,因其完善的安全体制和丰富的功能成为当今流行的安全管理框架。

引入

在小项目中,我们可以直接使用filter实现一个简单过滤器实现用户认证和授权,而且还有易上手的shiro,相比shiro,springsecurity功能非常完善且社区资源丰富,适用于大中型项目。

作用

  • SpringSecurity作为安全框架,核心功能必然是认证(判断用户是否能登录)和授权(赋予某些权限)
  • 与springboot等spring系列结合较为紧密,使用起来更加方便
  • 全面的权限控制

官方文档(Spring Security)

使用

环境搭建

  1. 首先启动一个springboot项目,添加maven依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 com.alibaba fastjson 1.2.69 com.auth0 java-jwt 3.4.0 org.projectlombok lombok com.baomidou mybatis-plus-boot-starter 3.4.3 mysql mysql-connector-java ```
2. 创建一个Controller测试整体项目是否启动成功
```java
@RestController
@RequestMapping("/test")
public class controller {
    @GetMapping("/hello")
    public String hello(){
        return "hello security";
    }
}    

在不加springsecurity的情况下,我们可以正常访问;加了SpringSecurity的依赖之后,再访问时就会跳转到登录界面,需要输入用户名为user以及随机生成的passsword才能正常访问。相当于过滤器生效。

基本原理

SpringSecurity本质上就是一个过滤链,重点是以下三个过滤器

  1. UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,比如我们刚才的登录,会获取并校验所提交表单的用户名,密码。主要源码为

深入浅出SpringSecurity--认证篇 2. ExceptionTranslationFilter:异常过滤器,用来处理在认证授权过程中抛出的异常,主要源码为

深入浅出SpringSecurity--认证篇 3. FilterSecurityInterceptor:  权限校验过滤器,主要源码为

深入浅出SpringSecurity--认证篇

整个项目中 我们需要书写一个配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login","/index.html").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        //把token校验过滤器添加到过滤器链中
//        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }



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

UserDetailsService接口使用

在实际开发工程中,我们需要直接到数据库中查询用户信息进行用户认证而不是由SpringSecurity生成,我们可以通过实现UserDetailsService接口重写loadUserByUsername() 方法实现自定义逻辑控制认证逻辑。如

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
   @Autowired
   private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        //如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        //授权 role必须是以下格式 不能为null
        List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");

        return new LoginUser(user);

    }
}

那么这个方法如何被调用呢?

我们可以按照正常的MVC调用过程 在controller层中调用service的方法

Controller层代码

@RestController
public class UserController {

    @Autowired
    private LoginServcie loginServcie;

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

    @GetMapping("/user/login")
    public Result login(@Param("username") String username, String password){
        User user = new User(username,password);
        return loginServcie.login(user);
    }
}

Service层代码

@Service
public class LoginServiceImpl implements LoginServcie {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public Result login(User user) {

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }
        //使用userid生成token

        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        HashMap<String, String> map = new HashMap<>();
        map.put("userId", userId);
        String jwt = JwtUtils.getToken(map);
        //authenticate存入redis
        redisTemplate.opsForValue().set("login:"+userId,jwt);
        //把token响应给前端
        return  Result.success("登录成功");
    }
          //登出
         @Override
         public Result logout() {
             Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
             LoginUser loginUser = (LoginUser) authentication.getPrincipal();
             Long userid = loginUser.getUser().getId();
             redisTemplate.delete("login:"+userid);
             return  Result.success("退出成功");
        }
}

LoginUser类的作用: 由于UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

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

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

在loginServiceImpl运行到

Authentication authenticate = authenticationManager.authenticate(authenticationToken);

就会去调用UserDetailsServiceImpl中 loadUserByUsername方法 进行查询数据库并认证的过程,这样就完成了认证的过程

补充 BCryptPasswordEncoder

我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。

  • 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。

  • 我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


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

}

Spring Security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥对密码进行加密。SHA系列是Hash算法,采用Hash处理,加密之后不能解密,其过程是不可逆的。

(1)加密(encode):注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。

(2)密码匹配(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。

随机盐

BCryptPasswordEncoder的 encode 方法对原文加密,会产生随机数的盐salt,所以每次加密得到的密文都是不同的

那么每次不同,如何实现可以比较出原文和密文是否相匹配呢,重点就是在密文里面包含了随机生成的 salt, 加密和解密都是调用的一样的方法。 所以可以比较出来

我们知道 密文是由原文和盐根据算法生成的,即密文中包含了密文(可以通过debug看到), 也就是说知道了盐, 也知道了原文。 用这个原文和 盐在生成一次密文,如果这个得出的密文等于比较的密文,那么说明这个密文就是这个原文生成的

小结

关于认证这块,其实可以感受到在认证过程中调用的一系列的过滤器链,实现认证过程。