SpringBoot整合Spring Security
1 快速入门
- 在项目中直接引入Spring Security的依赖
<!--springSecurity-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 启动项目,访问接口
 ![]() 
 引入Security之前在浏览器可以直接访问
 但引入了Security之后访问这个接口跳转到了一个登陆页面
 ![]() 
 引入Security之后访问系统所以接口都需要认证,没有登陆需要先登陆
 这个页面默认的用户名为 user ,密码在控制台可以看到
 ![]() 
 输入错误的密码会弹出提示
 ![]() 
 输入控制台中的正确密码就可以访问到接口了
2 Spring Security快速入门认证流程原理

- 前端提交用户名、密码先到了UsernamePasswordAuthenticationFilter中
- 在UsernamePasswordAuthenticationFilter中将用户名密码封装为Authentication对象
 ![]() 
- 调用ProviderManager中的authenticate()方法进行认证
 ![]() 
- 在ProviderManager中调用AbstractUserDetailsAuthenticationProvider中的authenticate方法
 ![]() 
- 在AbstractUserDetailsAuthenticationProvider中调用它的子类DaoAuthenticationProvider重写的retrieveUser方法
 ![]() 
- 在DaoAuthenticationProvider中会去调用实现了UserDeatilsService接口的InMemoryUserDetailsManager中的loadUserByUsername方法
 ![]() 
- InMemoryUserDetailsManager是在内存中查找
 ![]() 
- 找到用户之后将其用户信息封装成UserDetails对象返回给DaoAuthenticationProvider
- DaoAuthenticationProvider再将UserDetails对象返回给AbstractUserDetailsAuthenticationProvider
- AbstractUserDetailsAuthenticationProvider中再校验UserDetails对象的四种是否可用状态
 ![]() 
 然后调用DaoAuthenticationProvider的additionalAuthenticationChecks方法将UserDetails对象中的密码与前台传递的密码进行比较
 ![]() 
 如果正确则将UserDetails对象中的信息设置到Authentication对象中返回
3 从数据库中查询用户
1 问题分析
我们自己的项目肯定是不能从内存中去查询用户信息的,需要从库表中查询用户
从上面的流程我们可以看到Security查询用户是调用实现了UserDetilsService接口的InMemoryUserDetailsManager中的loadUserByUsername方法查询根据用户名查询用户
那么我们就可以通过自己实现UserDetilsService接口,重写其中的loadUserByUsername方法,在方法中去库表中查询用户信息,然后返回
2 实现步骤
1 因为loadUserByUsername返回的是UserDetails类型对象,所以我们需要自定义一个对象实现UserDetails,将学生作为其中的属性
public class LoginUser implements UserDetails {
    private Student student;
    public LoginUser() {
    }
    public LoginUser(Student student) {
        this.student = student;
    }
    /**
     * 获取用户的权限信息,后边需要授权的时候,就要在这个类中定义一个属性来存储权限
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
    @Override
    public String getPassword() {
        return student.getPassword();
    }
    @Override
    public String getUsername() {
        return student.getSname();
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}
2 实现UserDetilsService接口注入容器
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    StudentMapper studentMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //从数据库中 根据username查询用户信息
        Student stu = studentMapper.findById(Integer.parseInt(username));
        //查询为空,则抛出异常
        if (Objects.isNull(stu)){
            throw new RuntimeException("用户不存在");
        }
        //封装成UserDetails对象返回
        LoginUser loginUser = new LoginUser(stu);
        return loginUser;
    }
}
3 库表数据

注意:Security默认的PasswordEncoder要求库中密码前有类型标识,需要如果你想让用户的密码是明文存储,需要在密码前加{noop}
4 测试
访问接口

输入库中的用户名密码

debug一下看看查询到的用户

成功访问到

4 前端请求登陆
问题分析
Security默认的是将登陆页面中的用户名密码给到UsernamePasswordAuthenticatieFilter中,然后将用户名密码封装成Authentication对象调用ProviderManager的authenticate方法进行认证
那么我们的前后端分离项目中,前端要带着用户名密码请求我们后端自定义的登陆接口,我们就可以在自己的接口中将前端传过来的用户名密码封装成Authentication对象,然后我们再自己调用ProviderManager的authenticate方法进行认证
注意这里我们需要用到Security中的ProviderManager,所以需要在Security的配置类中配置一个ProviderManager的Bean
1 自定义一个登陆接口
@RestController
@RequestMapping("/system")
public class CommonController {
    @Autowired
    CommonService commonService;
    @PostMapping("/login")
    public String login(@RequestBody Student student){
        //调用service进行登陆认证
        return commonService.login(student);
    }
}
@Service
public class CommonService {
    @Autowired
    AuthenticationManager authenticationManager;
    public String login(Student student) {
        //将请求的用户名密码封装成Authentication对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(student.getSid(),student.getPassword());
        //调用providerManager的authenticate进行认证
        Authentication authenticateStudent = authenticationManager.authenticate(authenticationToken);
        //从认证结果中取出用户
        LoginUser logStudent = (LoginUser) authenticateStudent.getPrincipal();
        //TODO()自定义其它操作
        return "登陆成功!";
    }
}
2 Security配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.formLogin();//security默认登陆页面,前后端分离项目用不到
        http
                .csrf().disable()//cxrf为了防止跨站伪造请求攻击,认证时还会认证一个csrf_token。前后端分离项目是天然能防止的,所以必须关闭csrf,否则认证不了,
                .authorizeRequests()//配置请求认证
                .antMatchers("/system/login").anonymous() //允许登陆接口可以匿名访问
                .anyRequest().authenticated();  //其它所有请求都要认证
    }
    /**
     * Security默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
     * 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
     * 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    /**
    *AuthenticationManager 是接口,ProviderManager是其实现类
    */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
3 修改库中密码
PasswordEncoder使用BCryptPasswordEncoder的话,库表里的密码就得是经过BCryptPasswordEncoder加密的,所以我们要把库表中的密码使用BCryptPasswordEncoder加密后存进去

4 模拟前端调用

5 登陆认证失败返回
如果密码输入错误的

只会返回403状态码,访问失败
我们怎样使登陆认证失败也能返回我们期望的数据格式呢?
要实现这个功能我们需要知道SpringSecurity的异常处理机制。
 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
 所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
1 自定义认证失败处理的实现类
@Component
public class AuthenticationFaliure implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //设置状态码
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        //设置编码格式
        response.setCharacterEncoding("utf-8");
        //返回结果
        PrintWriter writer = response.getWriter();
        writer.print(authException.getMessage());
    }
}
2 配置给SpringSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    public AuthenticationEntryPoint authenticationEntryPoint;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.formLogin();//security默认登陆页面,前后端分享项目用不到
        http
                .csrf().disable()//cxrf为了防止跨站伪造请求攻击,认证时还会认证一个csrf_token。前后端分离项目是天然能防止的,所以必须关闭csrf,否则认证不了,
                .authorizeRequests()//配置请求认证
                .antMatchers("/system/login").anonymous() //允许登陆接口可以匿名访问
                .anyRequest().authenticated();  //其它所有请求都要认证
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint);
    }
    /**
     * Security默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
     * 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
     * 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
3 输入错误的密码测试

这个提示是Security校验密码后抛出的
4 输入错误的用户测试

这是个我们在自定义UserDetailsService中根据用户名没有查到用户自己抛出的

或者我们也可以不实现Security的异常处理,自己用try-catch来捕获异常进行处理返回
 
                    
                     
                    
                 
                    
                












 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号