SpringBoot 整合 Spring Security 实现权限控制

1,672 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第27天,点击查看活动详情

1. 引入 Spring Security 依赖

SpringBoot 中使用 Spring Security 时,只需要引入对应的依赖即可

<!--Security依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • 项目中引入依赖后,便会对项目服务请求时增加默认的登录功能
  • 默认账户名称为 user,密码在控制台生成并输出

2. 使用 Spring Security 自定义登录信息

随机生成的密码是动态的,每次运行项目会重新生成 如果想要自定义配置登录用户名和密码,可以通过以下方式进行。

  1. 在 application.properties 配置文件中配置
    • spring.security.user.name=root
    • spring.security.user.password=root
  2. 通过代码配置到内存中
    • 创建 Spring Security 配置类继承 WebSecurityConfigurerAdapter,并重写其中 configure 方法,来定义用户信息
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { 
    @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { 
        //下面这两行配置表示在内存中配置了两个用户 
        auth.inMemoryAuthentication()
        .withUser("javaboy").roles("admin")
        .password("$2a$10$OR3VSksVAmCzc.7WeaRPR.t0wyCsIj24k0Bne8iKWV1o.V9wsP8Xe")
        .and()
        .withUser("lisi").roles("user")
        .password("$2a$10$p1H8iWa8I4.CA.7Z8bwLjes91ZpY.rYREGHQEInNtAp4NzL6PLKxi");
    }
    
    @Bean 
    PasswordEncoder passwordEncoder() { 
        return new BCryptPasswordEncoder(); 
    } 
}
  • 如果同时使用配置文件和 Java 代码配置,则配置文件中配置不生效
  • 其中 passwordEncoder 方法用来对密码进行加密,Spring5 之后强制要求对密码加密,不加密的 NoOpPasswordEncoder 实例对象已经过时,不建议使用。
  • 如果不定义加密方法,则登录时会报错: There is no PasswordEncoder mapped for the id "null"
  • BCryptPasswordEncoder 加密方法对于同样的值,每次生成的加密结果都不同
  1. 使用代码加载数据库中的数据
    • 同样是在 configure 方法中,使用 auth 来指定从对应的用户信息类中读取用户信息
//如果需要改变认证的用户信息来源,我们可以实现UserDetailsService
auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder);

3. configure(AuthenticationManagerBuilder auth) 配置

上一节说到可以通过自定义配置文件继承 WebSecurityConfigurerAdapter 类来定义登录用户信息,通过重写 configure(AuthenticationManagerBuilder auth) 方法。

继承 WebSecurityConfigurerAdapter 类后还可以重写 configure(HttpSecurity http) 方法来自定义登录相关信息,如登录成功或失败后响应信息。

注意:

  • 如果不重写该 configure 方法或调用 super.configure() 逻辑,会执行默认的登录认证逻辑。
  • 如果重写了该方法,则需要自定义配置登录逻辑,比如登录页面等信息,如方法中无内容则不会绑定对应登录认证逻辑。

4. 其他配置内容

4.1 Handler

对于未登录、登录成功、登录失败等处理逻辑,除了重写 configure(HttpSecurity http) 方法来定义外,还可以通过定义 Handler 实现对应的接口来完成。

  • 实现AuthenticationEntryPoint接口,当匿名请求需要登录的接口时,拦截处理
    • 未登录时,访问了需要登录的资源
  • 实现AuthenticationSuccessHandler接口,当登录成功后,该处理类的方法被调用
    • 登录成功后进行 token、session 等处理并返回
  • 实现AuthenticationFailureHandler接口,当登录失败后,该处理类的方法被调用
    • 登录失败
  • 实现AccessDeniedHandler接口,当登录后,访问接口没有权限的时候,该处理类的方法被调用
    • 登录成功后,访问需要权限的资源
  • 实现LogoutSuccessHandler接口,注销的时候调用
    • 注销登录,如果需要过期 token,可以使用 token + session 的方法

4.2 OncePerRequestFilter

Security 中提供了 OncePerRequestFilter,用于获取请求头中的相关信息。

  • 如果请求头中带有服务返回的 token 信息,就可以进行认证逻辑处理,并将处理结果放到 Security 上下文中,最后请求放行后进入对应业务处理;
  • 如果 token 无效或没有相关 token 信息,则放行后进入未登录或未授权逻辑。

4.3 UserDetails

Security 中默认提供了一个 User 类,用来定义最简单的用户信息,并在实际操作时使用 UserDetail 来获取相关用户信息。

如果需要自定义用户信息类,可以通过实现 UserDetails 进行用户字段的扩展,以此来代替默认实现的 User 类。

4.4 UserDetailsService

Security 中的 UserDetailsService 是针对 UserDetails 内容的操作逻辑,逻辑可以指定根据用户名,到对应数据库中获取完整用户信息,并将内容填充到 UserDetails 对象中。

可以自定义处理逻辑类来实现 UserDetailsService,并重写其中的 loadUserByUserName 方法,如果自定义用户类实现了 UserDetails,则可以将用户信息赋值到对应实现类对象中。

5. 注册自定义配置

如果实现了默认的 handler、filter,则需要在 configure(HttpSecurity http) 中注册。

5.1 绑定匿名 handler 和 无权限 handler

http.exceptionHandling()
        // 如果自定义 handler 实现了 AuthenticationEntryPoint,可以传入
        .authenticationEntryPoint(new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                // 未登录处理逻辑
            }
        })
        // 如果自定义 handler 实现了 AccessDeniedHandler,可以传入
        .accessDeniedHandler(new AccessDeniedHandler() {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 无权限处理逻辑
    }
});

5.2 注册 filter

  • UsernamePasswordAuthenticationFilter 用来进行认证
http.addFilterBefore(new OncePerRequestFilter() {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取登录后请求信息的逻辑
    }
}, UsernamePasswordAuthenticationFilter.class);

5.3 绑定登录成功和失败 handler

http.formLogin()
        // 自定义登录页面,未登录时访问需要登录的接口,会自动跳转到该页面
        .loginPage("/login")
        // 登录处理接口
        .loginProcessingUrl("/doLogin")
        // 定义登录时 用户名和密码对应的 key 值,默认为 username 和 password
        .usernameParameter("userName")
        .passwordParameter("password")
        // 登录 handler,如果实现了 AuthenticationSuccessHandler,可以使用
        .successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                response.setContentType("application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                out.write("success");
                out.flush();
            }
        })
        // 失败 handler,如果实现了 AuthenticationFailureHandler, 可以使用
        .failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                response.setContentType("application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                out.write("fail");
                out.flush();
            }
        })
        // 和表单登录相关的接口统统都直接通过,permitAll,表示以上所有请求会放行
        .permitAll();

5.4 登出 handler

http.logout()
    .logoutUrl("/logout")
    // 登出逻辑 handler,如果实现了 LogoutSuccessHandler,可以使用
    .logoutSuccessHandler(new LogoutSuccessHandler() {
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write("logout success");
            out.flush();
        }
    })
    .permitAll();

5.5 and() 和 permitAll()

对于 http 的相关配置,可以使用 and() 方法来分隔配置,and() 代表之前配置进行提交,并重新获取 http 对象进行配置处理。

permitAll() 则表示之前的请求会全部放行通过,可以配合 antMatchers() 来匹配对应的资源路径进行处理。

  • .antMatchers("/login","/**/register/**").permitAll(),表示匹配路径放行
  • anyRequest().authenticated(),表示所有请求都会进行登录认证,但是如果使用 permitAll() 进行了单独配置,则会不需要进行登录认证。
  • 路径认证配置都需要在 http.authorizeRequests() 之后进行配置

5.6 忽略拦截配置

如果一个接口请求地址不想要进行登录认证拦截,则可以通过

  1. 设置地址匿名访问
  2. 在认证登录中过滤掉该地址,使用 configure(WebSecurity web) 方法,定义不受 Spring Security 约束的路径地址
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override 
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/path");
    } 
}