栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

SpringBoot安全框架Spring Security

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

SpringBoot安全框架Spring Security

SpringBoot安全框架Spring Security

Spring Boot针对Spring Security提供了自动化配置方案,因此可以使Spring Security非常容易地整合进Spring Boot项目中,这也是在Spring Boot项目中使用Spring Security的优势。

Spring Security的基本配置 1. 基本用法

基本整合步骤如下。

1.1 创建项目,添加依赖

创建一个 Spring Boot Web项目,然后添加spring-boot-starter-security依赖即可,代码如下:


    org.springframework.boot
    spring-boot-starter-web


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

只要开发者在项目中添加了spring-boot-starter-security依赖,项目中所有资源都会被保护起来。

1.2 添加hello接口

接下来在项目中添加一个简单的/hello接口,内容如下:

@RestController
public class MyController {
    @GetMapping("/hello")
    public String hello(){
        return "Hello";
    }
}
1.3 启动项目测试

接下来启动项目,启动成功后,访问/hello接口会自动跳转到登录页面,这个登录页面是由Spring Security提供的,如图所示。

默认的用户名是user,默认的登录密码则在每次启动项目时随机生成,查看项目启动日志,如图所示。

从项目启动日志中可以看到默认的登录密码,登录成功后,用户就可以访问“/hello”接口了。

2. 配置用户名和密码

如果开发者对默认的用户名和密码不满意,可以在application.properties中配置默认的用户名、密码以及用户角色,配置方式如下:

spring.security.user.name=suo
spring.security.user.password=123
spring.security.user.roles=admin

当开发者在 application.properties 中配置了默认的用户名和密码后,再次启动项目,项目启动日志就不会打印出随机生成的密码了,用户可直接使用配置好的用户名和密码登录,登录成功后,用户还具有一个角色——admin。

3. 基于内存的认证

当然,开发者也可以自定义类继承自WebSecurityConfigurerAdapter,进而实现对Spring Security更多的自定义配置,例如基于内存的认证,配置方式如下:

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication ()
                .withUser ( "admin" ).password("123" ) .roles ( "ADMIN","USER").and ()
                .withUser ( "sang" ) .password ("123" ).roles ( "USER");
    }
}

代码解释:

  • 自定义MyWebSecurityConfig继承自WebSecurityConfigurerAdapter ,并重写configure(AuthenticationManagerBuilder auth)方法,在该方法中配置两个用户,一个用户名是admin,密码123,具备两个角色ADMIN和USER;另一个用户名是sang,密码是123,具备一个角色USER。
  • 本案例使用的Spring Security版本是5.6.2,在Spring Security 5.x中引入了多种密码加密方式,开发者必须指定一种,本案例使用NoOpPasswordEncoder,即不对密码进行加密,单不推荐这种方式加密。

注意:基于内存的用户配置在配置角色时不需要添加“ROLE_”前缀,这点和基于数据库的认证有差别。

4. HttpSecurity

虽然现在可以实现认证功能,但是受保护的资源都是默认的,而且也不能根据实际情况进行角色管理,如果要实现这些功能,就需要重写WebSecurityConfigurerAdapter 中的另一个方法,代码如下:

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("123").roles("ADMIN", "USER").and()
                .withUser("sang").password("123").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/** ").hasRole("ADMIN")
                .antMatchers("/user/**")
                .access("hasAnyRole('ADMIN','USER')").antMatchers("/ db/** ")
                .access("hasRole ('ADMIN') and hasRole ('DBA') ").anyRequest()
                .authenticated().and()
                .formLogin()
                .loginProcessingUrl(" / login").permitAll()
                .and()
                .csrf()
                .disable();
    }
}

代码解释:

  • 首先配置了三个用户,root用户具备ADMIN和 DBA 的角色,admin用户具备ADMIN和USER的角色,sang用户具备USER的角色。

  • 第18行调用authorizeRequests()方法开启HttpSecurity 的配置,第19~24行配置分别表示用户访问/admin/模式的URL必须具备ADMIN的角色;用户访问/user/ 模式的URL必须具备ADMIN或USER的角色;用户访问db/模式的URL必须具备ADMIN和DBA的角色。

  • 第25、26行表示除了前面定义的URL模式之外,用户访问其他的URL都必须认证后访问(登录后访问)。

  • 第27~30行表示开启表单登录,即读者一开始看到的登录页面,同时配置了登录接口为“/login”,即可以直接调用“/login”接口,发起一个POST请求进行登录,登录参数中用户名必须命名为username,密码必须命名为password,配置loginProcessingUrl接口主要是方便Ajax或者移动端调用登录接口。最后还配置了permitAll,表示和登录相关的接口都不需要认证即可访问。

  • 第32、33行表示关闭csrf。

配置完成后,接下来在Controller中添加如下接口进行测试:

@RestController
public class HelloController {
    @GetMapping("/admin/hello")

    public String admin() {
        return "hello admin!";
    }

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

    @GetMapping(" /db/hello")
    public String dba() {
        return "hello dba!";
    }

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

根据上文的配置,“/admin/hello”接口root和 admin用户具有访问权限;“luser/hello”接口admin和 sang用户具有访问权限;“ldb/hello”路径则只有root用户具有访问权限。浏览器中的测试比较容易,这里不再赘述。

5. 登录表单详细配置

迄今为止,登录表单一直使用Spring Security提供的页面,登录成功后也是默认的页面跳转,但是,前后端分离正在成为企业级应用开发的主流,在前后端分离的开发方式中,前后端的数据交互通过JSON进行,这时,登录成功后就不是页面跳转了,而是一段JSON提示。要实现这些功能,只需要继续完善上文的配置,代码如下:

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("123").roles("ADMIN", "USER").and()
                .withUser("sang").password("123").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/** ").hasRole("ADMIN")
                .antMatchers("/user/**")
                .access("hasAnyRole('ADMIN','USER') ").antMatchers(" / db/** ")
                .access("hasRole ('ADMIN') and hasRole ('DBA') ").anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/loginPage")
                .loginProcessingUrl("/login")
                .usernameParameter("name")
                .passwordParameter("passwd")
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        Object principal = authentication.getPrincipal();
                        response.setContentType("application/json; charset=utf-8");
                        PrintWriter out = response.getWriter();
                        response.setStatus(200);
                        Map map = new HashMap<>();
                        map.put("status", 200);
                        map.put("msg", principal);
                        ObjectMapper om = new ObjectMapper();
                        out.write(om.writeValueAsString(map));
                        out.flush();
                        out.close();
                    }
                })
                .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();
                        response.setStatus(401);
                        Map map = new HashMap<>();
                        map.put("status", 401);
                        if (exception instanceof LockedException) {
                            map.put("msg", "账户被锁定,登录失败! ");
                        } else if (exception instanceof BadCredentialsException) {
                            map.put("msg", "账户名或密码输入错误,登录失败! ");
                        } else if (exception instanceof DisabledException) {
                            map.put("msg", "账户被禁用,登录失败! ");
                        } else if (exception instanceof AccountExpiredException) {
                            map.put("msg", "账户已过期,登录失败!");
                        } else if (exception instanceof CredentialsExpiredException) {
                            map.put("msg", "密码已过期,登录失败!");
                        } else {
                            map.put("msg", "登录失败! ");
                        }
                        ObjectMapper om = new ObjectMapper();
                        out.write(om.writeValueAsString(map));
                        out.flush();
                        out.close();
                    }
                }).permitAll()
                .and()
                .csrf()
                .disable();
    }
}

代码解释:

  • 第25行配置了loginPage,即登录页面,配置了loginPage后,如果用户未获授权就访问一个
    需要授权才能访问的接口,就会自动跳转到login_page页面让用户登录,这个login_page就是开发者自定义的登录页面,而不再是Spring Security提供的默认登录页。

  • 第26行配置了loginProcessingUrl,表示登录请求处理接口,无论是自定义登录页面还是移动端登录,都需要使用该接口。

  • 第27、28行定义了认证所需的用户名和密码的参数名,默认用户名参数是username,密码参数是password,可以在这里自定义。

  • 第29~44行定义了登录成功的处理逻辑。用户登录成功后可以跳转到某一个页面,也可以返回一段JSON,这个要看具体业务逻辑,本案例假设是第二种,用户登录成功后,返回一段登录成功的JSON。onAuthenticationSuccess方法的第三个参数一般用来获取当前登录用户的信息,在登录成功后,可以获取当前登录用户的信息一起返回给客户端。

  • 第45~70行定义了登录失败的处理逻辑,和登录成功类似,不同的是,登录失败的回调方法里有一个AuthenticationException参数,通过这个异常参数可以获取登录失败的原因,进而给用户一个明确的提示。

配置完成后,使用Postman进行登录测试,如图所示。

登录请求参数用户名是name,密码是passwd,登录成功后返回用户的基本信息,密码已经过滤掉了。如果登录失败,也会有相应的提示,如图所示。

6. 注销登录配置

如果想要注销登录,也只需要提供简单的配置即可,代码如下:

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("123").roles("ADMIN", "USER").and()
                .withUser("sang").password("123").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/** ").hasRole("ADMIN")
                .antMatchers("/user/**")
                .access("hasAnyRole('ADMIN','USER') ").antMatchers(" / db/** ")
                .access("hasRole ('ADMIN') and hasRole ('DBA') ").anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/loginPage")
                .loginProcessingUrl("/login")
                .usernameParameter("name")
                .passwordParameter("passwd")
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        Object principal = authentication.getPrincipal();
                        response.setContentType("application/json; charset=utf-8");
                        PrintWriter out = response.getWriter();
                        response.setStatus(200);
                        Map map = new HashMap<>();
                        map.put("status", 200);
                        map.put("msg", principal);
                        ObjectMapper om = new ObjectMapper();
                        out.write(om.writeValueAsString(map));
                        out.flush();
                        out.close();
                    }
                })
                .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();
                        response.setStatus(401);
                        Map map = new HashMap<>();
                        map.put("status", 401);
                        if (exception instanceof LockedException) {
                            map.put("msg", "账户被锁定,登录失败! ");
                        } else if (exception instanceof BadCredentialsException) {
                            map.put("msg", "账户名或密码输入错误,登录失败! ");
                        } else if (exception instanceof DisabledException) {
                            map.put("msg", "账户被禁用,登录失败! ");
                        } else if (exception instanceof AccountExpiredException) {
                            map.put("msg", "账户已过期,登录失败!");
                        } else if (exception instanceof CredentialsExpiredException) {
                            map.put("msg", "密码已过期,登录失败!");
                        } else {
                            map.put("msg", "登录失败! ");
                        }
                        ObjectMapper om = new ObjectMapper();
                        out.write(om.writeValueAsString(map));
                        out.flush();
                        out.close();
                    }
                }).permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .clearAuthentication(true)
            	.invalidateHttpSession(true)
                .addLogoutHandler(new LogoutHandler() {
                    @Override
                    public void logout(HttpServletRequest req,
                                       HttpServletResponse resp, Authentication auth) {
                    }
                })
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override

                    public void onLogoutSuccess(HttpServletRequest req,
                                                HttpServletResponse resp, Authentication auth)
                            throws IOException {
                        resp.sendRedirect("/login_page");
                    }
                })
                .and()
                .csrf()
                .disable();
    }
}

代码解释:

  • 第73行表示开启注销登录的配置。
  • 第74行表示配置注销登录请求URL为“/logout”,默认也是“/logout”。
  • 第75行表示是否清除身份认证信息,默认为 true,表示清除。
  • 第76行表示是否使Session失效,默认为true。
  • 第77行配置一个LogoutHandler,开发者可以在LogoutHandler中完成一些数据清除工作,例如Cookie 的清除。Spring Security提供了一些常见的实现,如图所示。

  • 第83行配置一个LogoutSuccessHandler,开发者可以在这里处理注销成功后的业务逻辑,例如返回一段JSON提示或者跳转到登录页面等。
7. 多个HttpSecurity

如果业务比较复杂,开发者也可以配置多个HttpSecurity,实现对WebSecurityConfigurerAdapter的多次扩展,代码如下:

@Configuration
public class MultiHttpSecurityConfig {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("123").roles("ADMIN", "USER").and()
                .withUser("sang").password("123").roles("USER");
    }

    @Configuration
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher(" / admin/**").authorizeRequests()
                    .anyRequest().hasRole("ADMIN");
        }

        @Configuration
        public static class otherSecurityConfig extends WebSecurityConfigurerAdapter {
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.authorizeRequests()
                        .anyRequest().authenticated().and()
                        .formLogin()
                        .loginProcessingUrl("/login").permitAll()
                        .and()
                        .csrf().disable();
            }
        }
    }
}

代码解释:

  • 配置多个HttpSecurity时,MultiHttpSecurityConfig 不需要继承WebSecurityConfigurerAdapter,在MultiHttpSecurityConfig 中创建静态内部类继承WebSecurityConfigurerAdapter 即可,静态内部类上添加@Configuration注解和@Order 注解,@Order注解表示该配置的优先级,数字越小优先级越大,未加@Order注解的配置优先级最小。
  • 第15到22行配置表示该类主要用来处理“admin/**”模式的URL,其他的URL将在第24到35行配置的HttpSecurity中进行处理。
8. 密码加密 8.1 为什么要加密

2011年12月21日,有人在网络上公开了一个包含600万个 CSDN用户资料的数据库,数据全部为明文存储,包含用户名、密码以及注册邮箱。事件发生后,CSDN在微博、官方网站等渠道发出了声明,解释说此数据库是2009年备份所用的,因不明原因泄露,已经向警方报案,后又在官网发出了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入这次事件中。整个事件中最触目惊心的莫过于CSDN把用户密码明文存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄露就会造成很大的安全隐患。有了这么多前车之鉴,我们现在做系统时,密码都要加密处理。

8.2 加密方案

密码加密一般会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中创建数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据会使得数据库记录更难找到。我们常用的散列函数有MD5消息摘要算法、安全散列算法( Secure Hash Algorithm)。

但是仅仅使用散列函数还不够,为了增加密码的安全性,一般在密码加密过程中还需要加盐,所谓的盐可以是一个随机数,也可以是用户名,加盐之后,即使密码明文相同的用户生成的密码,密文也不相同,这可以极大地提高密码的安全性。但是传统的加盐方式需要在数据库中有专门的字段来记录盐值,这个字段可能是用户名字段(因为用户名唯一),也可能是一个专门记录盐值的字段,这样的配置比较烦琐。Spring Security提供了多种密码加密方案,官方推荐使用BCryptPasswordEncoder,BCryptPasswordEncoder使用BCrypt 强哈希函数,开发者在使用时可以选择提供 strength和 SecureRandom实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为2^strength。strength取值在4~31之间,默认为10。

8.3 实践

在Spring Boot中配置密码加密非常容易,只需要修改上文配置的 PasswordEncoder这个Bean的实现即可,代码如下:

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

创建BCryptPasswordEncoder时传入的参数10就是strength,即密钥的迭代次数(也可以不配置,默认为10)。同时,配置的内存用户的密码也不再是123了,代码如下:

 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     auth.inMemoryAuthentication()
.withUser("admin").password("$2a$10$t2V6WlZjKt4Lv/ByT7Eu3.7vEwoNQiuawVrUzjdlrY2j1QbmKWSS6").roles("ADMIN", "USER")
.and()
.withUser("suo").password("$2a$10$f6O0vzvn1qjbZucVQP7rVOHNP9Pz//zcruBC2Z5kMZdTTkxZSCa5K").roles("USER");

这里的密码就是使用BCryptPasswordEncoder 加密后的密码,虽然admin和suo加密后的密码不一样,但是明文都是123。配置完成后,使用admin/123或者suo/123就可以实现登录。本案例使用了配置在内存中的用户,一般情况下,用户信息是存储在数据库中的,因此需要在用户注册时对密码进行加密处理,代码如下:

@Service
public class RegService {
    public int reg(String username, String password) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
        String encodePasswod = encoder.encode(password);
        return saveToDB(username, encodePasswod);//saveToDb是保存到数据库的方法
    }
}

用户将密码从前端传来之后,通过调用BCryptPasswordEncoder实例中的encode方法对密码进行加密处理,加密完成后将密文存入数据库。

9. 方法安全

上文介绍的认证与授权都是基于URL 的,开发者也可以通过注解来灵活地配置方法安全,要使用相关注解,首先要通过@EnableGlobalMethodSecurity注解开启基于注解的安全配置:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class webSecurityConfig {
}

代码解释:

  • prePostEnabled=true会解锁@PreAuthorize和@PostAuthorize两个注解,顾名思义,@PreAuthorize注解会在方法执行前进行验证,而@PostAuthorize注解在方法执行后进行验证。
  • securedEnabled=true会解锁@Secured注解。

开启注解安全配置后,接下来创建一个MethodService进行测试,代码如下:

@Service
public class MethodService {
    @Secured("ROLE_ADMIN")
    public String admin() {
        return "hello admin";
    }

    @PreAuthorize("hasRole('ADMIN') and hasRole('DBA')")
    public String dba() {
        return "hello dba";
    }

    @PreAuthorize("hasAnyRole('ADMIN','DBA','USER')")
    public String user() {
        return "user";
    }
}

代码解释:

  • @Secured(“ROLE_ADMIN”)注解表示访问该方法需要ADMIN角色,注意这里需要在角色前加一个前缀“ROLE_”。
  • @PreAuthorize(“hasRole(‘ADMIN’) and hasRole(‘DBA’)”)注解表示访问该方法既需要ADMIN角色又需要DBA角色。
  • @PreAuthorize(“hasAnyRole('ADMIN, ‘DBA,USER’)”)表示访问该方法需要ADMIN、DBA或USER角色。
  • @PreAuthorize和@PostAuthorize中都可以使用基于表达式的语法。

最后,在Controller中注入Service并调用Service中的方法进行测试,这里比较简单,可以自行测试。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/825775.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号