SpringBoot整合Spring-Security 认证篇(保姆级教程)

本文项目基于以下教程的代码版本: https://javaxbfs.blog.csdn.net/article/details/135195636

代码仓库: https://gitee.com/skyblue0678/springboot-demo

为了跟shiro区别开,新建了一个分支:

SpringBoot整合Spring-Security 认证篇(保姆级教程)_第1张图片

目录

1、友善问候一下 Spring Security

⭐2、 POM依赖

3、登录

⭐4、根据账号从DB中获取用户实体

5、校验密码是否正确

6、全局异常返回

⭐7、测试

8、细说spring security


1、友善问候一下 Spring Security

Spring Security是Spring家族中的安全框架,可以用来做用户验证和权限管理等。Spring Security是一款重型框架,不过功能十分强大。

一般来说,如果项目中需要进行权限管理,具有多个角色和多种权限,我们可以使用Spring Security。如果是较为简单的项目,只需要控制一下某些接口只有登录后才能访问,则可以使用Shiro框架,Shiro也是一款安全框架,它是一款轻量级框架,功能没有Spring Security多,但使用起来要简单不少。

也就是说,Spring Security的颗粒度更细。

SpringSecurity 采用的是责任链的设计模式,是一堆过滤器链的组合,它有一条很长的过滤器链。

不过我们不需要去仔细了解每一个过滤器的含义和用法,只需要搞定以下几个问题即可:怎么登录、怎么校验账户、认证失败处理。

2、 POM依赖

没啥好说的,maven导入即可。



    org.springframework.boot
    spring-boot-starter-security

不写版本号,默认就会下载最新的版本。

3、登录

不管你用哪种权限框架,第一个要解决的问题就是登录。就是在我们的登录接口中,将账户密码委托给权限框架接管,让权限框架帮我们做校验和权限认证。

新建一个登录方法

@GetMapping("/security/login")
public String securityLogin(String username,String password){
    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username,password);
    // 使用authenticationManager调用loadUserByUsername获取数据库中的用户信息,
    Authentication authentication = authenticationManager.authenticate(authToken);
    if(authentication == null) {
        throw new RuntimeException("Login false");
    }

    //获取符合security规范的User
    SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();

    String token = jwtUtil.createToken(securityUser.getUser());
    return token;
}

这个方法的目的是验证用户的用户名和密码,并在验证成功后为该用户生成一个JWT。 UsernamePasswordAuthenticationToken就是我们委托框架帮我们托管的登录凭证,shiro框架也有类似的东西。 然后是:

Authentication authentication = authenticationManager.authenticate(authToken);

这就是spring security帮我们执行认证和授权的方法,最终返回一个认证结果。大家思考一下,我们正常登录的逻辑无非是四步走:

  1. 输入账号密码

  2. 根据账号从DB中获取用户实体

  3. 校验密码是否正确

  4. 校验成功,将用户生成token后返回

我们再回过来看这段代码,第2步和第3步没见到,只见到spring security帮我们做了。但是,这并不代表我们可以省略这两步,只是需要我们写在别的地方,仅此而已。

SpringBoot整合Spring-Security 认证篇(保姆级教程)_第2张图片

4、根据账号从DB中获取用户实体

这个步骤是不可能不写的,只是写到了别处。 先说个事儿哈,spring security中的用户概念,有自己的一套规则,不能直接用我们系统里面的User类。

如果我们要用spring security,就得实现他的用户接口:UserDetails。 所以,我们新建一个SecurityUser类:

// 使用Lombok库的@Data注解,自动生成getter、setter、equals、hashCode和toString方法
// 同样使用@NoArgsConstructor注解,自动生成无参构造函数
@Data
@NoArgsConstructor
public class SecurityUser implements UserDetails {

    // 使用聚合模式,将我们自己的User对象聚合到SecurityUser中
    // user字段存储了用户的一些信息,例如用户名和密码等
    private User user;

    // 覆盖UserDetails接口中的getAuthorities方法,返回用户的权限集合
    // 这里返回null,表示未实现获取权限的逻辑
    @Override
    public Collection getAuthorities() {
        return null;
    }

    // 覆盖UserDetails接口中的getPassword方法,返回用户的密码
    // 这里通过调用user对象的getPwd方法获取密码
    @Override
    public String getPassword() {
        return user.getPwd();
    }

    // 覆盖UserDetails接口中的getUsername方法,返回用户的用户名
    // 这里通过调用user对象的getUserName方法获取用户名,并注释说明有的地方可能会用email作为用户名,这里还是使用userName
    @Override
    public String getUsername() {
        // 用户名:有的地方可能会用email作为用户名,我们这还是userName
        return user.getUserName();
    }

    // 覆盖UserDetails接口中的isAccountNonExpired方法,判断账户是否过期
    // 这里直接返回true,表示账户不过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 覆盖UserDetails接口中的isAccountNonLocked方法,判断账户是否被锁定
    // 这里直接返回true,表示账户未被锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 覆盖UserDetails接口中的isCredentialsNonExpired方法,判断凭证是否过期
    // 这里直接返回true,表示凭证不过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 覆盖UserDetails接口中的isEnabled方法,判断用户是否启用
    // 这里直接返回true,表示用户启用状态为true
    @Override
    public boolean isEnabled() {
        return true;
    }
}

我们系统里面有自己的user了,但是为了适配,所以就聚合进来。

然后是如何查询DB呢,是不是得有个Service去查询,我们依据有自己的UserService了,但是很可惜,spring security有自己的规范,我们自己写的user Service,他不认,气死偶了。

没办法,重新写个Service,我们自己写都写了,也不能不管对不对?嗯,那还是聚合进来。

@Service
@Slf4j
public class UserDetailService implements UserDetailsService {

    @Resource
    UserService userService;

    /**
     * 根据用户名直接从DB中查询用户数据,作为登录校验的依据
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getByUsername(username);
        if (user == null) {
            log.info("username not found");
            throw new UsernameNotFoundException("username not found");
        }
        SecurityUser securityUser = new SecurityUser();
        securityUser.setUser(user);
        return securityUser;
    }

}

核心逻辑就是,我们还是用之前的方法拿到User,但为了适配,就塞到SecurityUser里面去。

UserDetailsService是spring security认可的接口,我们得实现这个接口,并且实现loadUserByUsername方法,这个方法在spring security的认证逻辑里面会用到。目的就是拿到DB中真实的User,跟我们登录的账号密码进行比对。

5、校验密码是否正确

为什么会有这一步呢,因为很多时候我们的密码是要进行加密的,但是我们登录肯定传的是明文密码,所以会需要转换后再去比对,否则肯定是校验失败了。

这个校验密码的逻辑,需要写在spring security的配置类中。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Resource
  UserDetailService userService;
  
   /**
     * 新增security账户
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return rawPassword.equals(encodedPassword);
            }
        });
    }

}

因为我们的项目并没有对密码加密,所以就直接比较了。

在LoginController中,我们用到了AuthenticationManager这个对象,需要在配置类中注册。

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    // 身份验证管理器, 直接继承即可.
    return super.authenticationManagerBean();
}

AuthenticationManager是Spring Security框架中的一个核心接口,它负责处理身份验证请求。在认证过程中,用户提交身份验证信息(如用户名和密码),AuthenticationManager会验证这些信息的有效性。

最后是路由的相关配置

@Override  
    protected void configure(HttpSecurity http) throws Exception {  
        http  
                // 禁用跨站请求伪造保护  
                .csrf().disable()   
                // 设置会话管理策略为无会话,因为我们使用token作为信息传递介质  
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)   
                .and()  
                // 进行认证请求的配置  
                .authorizeRequests()   
                // 将所有登入和注册的接口放开,这些都是无需认证就访问的  
                .antMatchers("/security/login").anonymous()   
                // 除了上面的那些,剩下的任何接口请求都需要经过认证  
                .anyRequest().authenticated()   
                .and()  
                // 允许跨域请求  
                .cors()   
                ;  
        // 在UsernamePasswordAuthenticationFilter之前添加JWT认证过滤器  
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);  
    }

这段代码是Spring Security框架中用于配置HTTP安全的部分。它主要涉及到跨站请求伪造(CSRF)的禁用、会话管理策略的设置、认证请求的配置以及跨域请求的允许。同时,还添加了一个JWT(JSON Web Token)认证过滤器。

因为我们项目用到了jwt,所以在进行账号密码验证之前,要先走jwt的过滤器。

jwt过滤器代码如下:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    JWTUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //token为空的话, 就不管它, 让SpringSecurity中的其他过滤器处理请求
            //请求放行
            filterChain.doFilter(request, response);
            return;
        }
        //token不为空时, 解析token
        User user = null;
        try {
            user = jwtUtil.verify(token);
        } catch (Exception e) {
            // 过滤器中抛出的异常,无法被统一异常捕获,所以在这里直接返回
            e.printStackTrace();
            Result result = new Result();
            result.setCode(403);
            result.setMsg("Token无效:" + e.getMessage());
            WebUtils.response(response,result);
            return;
        }
        SecurityUser securityUser = new SecurityUser();
        securityUser.setUser(user);
        //将用户安全信息存入SecurityContextHolder, 在之后SpringSecurity的过滤器就不会拦截
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(securityUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

流程可以参考这个图:

SpringBoot整合Spring-Security 认证篇(保姆级教程)_第3张图片

如果校验成功了,那么在登录方法中就会往下走,生成token返回,结束。

SpringBoot整合Spring-Security 认证篇(保姆级教程)_第4张图片

6、全局异常返回

认证过程中,难免出现各种异常,我们一般会做一个通用的返回,直接上代码,没啥好说的,网上一大堆

Result

Data
public class Result implements Serializable {

    private int code;
    private String msg;
    private Object data;

    public static Result succ(Object data) {
        return success(200, "操作成功", data);
    }

    public static Result error(String msg) {
        return error(400, msg, null);
    }

    public static Result success (int code, String msg, Object data) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

    public static Result error (int code, String msg, Object data) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
}

GlobalExceptionHandler

/**
 * 全局异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 400 错误:运行时异常
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result handler(RuntimeException e) {
        log.error("运行时异常:----------------{}", e.getMessage());
        return Result.error(e.getMessage());
    }

    /**
     * 403 错误:权限不足
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(value = AccessDeniedException.class)
    public Result handler(AccessDeniedException e) {
        log.info("security权限不足:----------------{}", e.getMessage());
        return Result.error("权限不足");
    }

    /**
     * 400 错误:异常请求-方法参数不匹配
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) {
        log.info("实体校验异常:----------------{}", e.getMessage());
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.error(objectError.getDefaultMessage());
    }

    /**
     * 400 错误:异常请求-非法参数
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) {
        log.error("Assert异常:----------------{}", e.getMessage());
        return Result.error(e.getMessage());
    }


}

7、测试

localhost:8080/security/login?username=jack&password=1

SpringBoot整合Spring-Security 认证篇(保姆级教程)_第5张图片

将token放到header,发送 localhost:8080

SpringBoot整合Spring-Security 认证篇(保姆级教程)_第6张图片

能正常返回就OK了。

如果token过期,就会报:

{
    "msg": "Token无效:The Token has expired on Tue Dec 26 15:43:42 CST 2023.",
    "code": 403
}

8、细说spring security

Spring Security 并非一个新生的事物,它最早不叫 Spring Security ,叫 Acegi Security,叫 Acegi Security 并不是说它和 Spring 就没有关系了,它依然是为 Spring 框架提供安全支持的。事实上,Java 领域的框架,很少有框架能够脱离 Spring 框架独立存在。

当 Spring Security 还叫 Acegi Security 的时候,虽然功能也还可以,但是实际上这个东西并没有广泛流行开来。最重要的原因就是它的配置太过于繁琐,当时网上流传一句话:“每当有人要使用 Acegi Security,就会有一个精灵死去。” 足见 Acegi Security 的配置是多么可怕。直到今天,当人们谈起 Spring Security 的时候,依然在吐槽它的配置繁琐。

后来 Acegi Security 投入 Spring 的怀抱,改名叫 Spring Security,事情才慢慢开始发生变化。新的开发团队一直在尽力简化 Spring Security 的配置,Spring Security 的配置相比 Acegi Security 确实简化了很多。但是在最初的几年里,Spring Security 依然无法得到广泛的使用。

直到有一天 Spring Boot 像谜一般出现在江湖边缘,彻底颠覆了 JavaEE 的世界。一人得道鸡犬升天,自从 Spring Boot 火了之后,Spring 家族的产品都被带了一把,Spring Security 就是受益者之一,从此飞上枝头变凤凰。

Spring Boot/Spring Cloud 现在作为 Java 开发领域最最主流的技术栈,这一点大家应该都没有什么异议,而在 Spring Boot/Spring Cloud 中做安全管理,Spring Security 无疑是最方便的。

你想保护 Spring Boot 中的接口,添加一个 Spring Security 的依赖即可,事情就搞定了,所有接口就保护起来了,甚至不需要一行配置。

所以说,因为 Spring Boot/Spring Cloud 火爆,让 Spring Security 跟着沾了一把光。

「有的人觉得 Spring Security 配置臃肿。」

如果是 SSM + Spring Security 的话,我觉得这话有一定道理。

但是如果是 Spring Boot 项目的话,其实并不见得臃肿。Spring Boot 中,通过自动化配置 starter 已经极大的简化了 Spring Security 的配置,我们只需要做少量的定制的就可以实现认证和授权了。

「有人觉得 Spring Security 中概念复杂。」这个是这样的,没错。

Spring Security 由于功能比较多,支持 OAuth2 等原因,就显得比较重量级,不像 Shiro 那样轻便。

但是如果换一个角度,你可能会有不一样的感受。

在 Spring Security 中你会学习到许多安全管理相关的概念,以及常见的安全攻击。这些安全攻击,如果你不是 web 安全方面的专家,很多可能存在的 web 攻击和漏洞你可能很难想到,而 Spring Security 则把这些安全问题都给我们罗列出来并且给出了相应的解决方案。

所以我说,我们学习 Spring Security 的过程,也是在学习 web 安全,各种各样的安全攻击、各种各样的登录方式、各种各样你能想到或者想不到的安全问题,Spring Security 都给我们罗列出来了,并且给出了解决方案,从这个角度来看,你会发现 Spring Security 好像也不是那么让人讨厌。

你可能感兴趣的:(spring,spring,boot,java)