SpringBoot 整合 Spring Security 实现权限控制

持续创作,加速成长!这是我参与「掘金日新计划 · 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 对象中。

扫描二维码关注公众号,回复: 14310915 查看本文章

可以自定义处理逻辑类来实现 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");
    } 
}

猜你喜欢

转载自juejin.im/post/7112091136709099550