springboot+security登录成功后访问api302_Spring Boot开启Spring Security

论坛 期权论坛 脚本     
已经匿名di用户   2022-2-7 16:34   2624   0

Spring Security是一款基于Spring的安全框架,主要包含认证和授权两大安全模块,和另外一款流行的安全框架Apache Shiro相比,它的功能更为强大,并且Shiro其实存在反编译漏洞,可能带来一定安全隐患。

Spring Security还可以轻松的自定义扩展以满足各种需求,对常见的Web安全攻击提供了防护支持。如果你的Web框架选择的是Spring Boot,那么在安全方面Spring Security会是一个不错的选择。因为Spring Boot使用了特定的方式来进行配置,从而使开发人员不再需要定义繁琐的XML配置文件,使用对应注解就可以管理对象的生命周期。

一般来说,常见的安全管理技术栈的组合是这样的:

  • SSM + Shiro

  • Spring Boot/Spring Cloud + Spring Security

我们来看下具体使用。

1.创建项目

在 Spring Boot 中使用 Spring Security 非常容易,引入依赖即可:

87fa6f5eeef630a3d2d647637c99d12d.png

pom.xml 中的 Spring Security 依赖:

<parent>    <groupId>org.springframework.bootgroupId>    <artifactId>spring-boot-starter-parentartifactId>    <version>2.4.0version>    <relativePath/> parent><dependencies>    <dependency>        <groupId>org.springframework.bootgroupId>        <artifactId>spring-boot-starter-securityartifactId>    dependency>     <dependency>         <groupId>org.springframework.bootgroupId>         <artifactId>spring-boot-starter-webartifactId>    dependency>dependencies>    

只要加入依赖,项目的所有接口都会被自动保护起来。

2.启动项目

我们创建一个 HelloController:

@RestControllerpublic class HelloController {    @GetMapping("hello")    public String hello() {        return "hello";    }}

访问 localhost:8080/hello ,被重定向到localhost:8080/login,必须登录之后才能访问。

75fc04adb218abede4d740d924ab1100.png

当用户从浏览器发送请求访问服务器地址(localhost:8080/)时,服务端会返回 302 响应码,让客户端重定向到 /login 页面,用户在 /login 页面登录,登陆成功之后,就会自动跳转到 /hello 接口。

默认情况下,登录的用户名是 user ,密码则是项目启动时随机生成的字符串,可以从启动的控制台日志中看到默认密码:

eb1f849fdcdeb2a31882a6ef17c862b3.png

另外,也可以使用postman来发送请求,使用postman发送请求时,可以将用户信息放在请求头中(这样可以避免重定向到登录页面)。

通过以上两种不同的登录方式,可以看出,Spring Security 支持两种不同的认证方式:

  • 可以通过 form 表单来认证

  • 可以通过 HttpBasic 来认证

我们可以通过一些配置将HTTP Basic认证修改为基于表单的认证方式。

创建一个配置类 BrowserSecurityConfig 继承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter这个抽象类并重写configure(HttpSecurity http)方法。WebSecurityConfigurerAdapter 是由Spring Security提供的Web应用安全配置的适配器:

@Configurationpublic class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {    @Override    protected void configure(HttpSecurity http) throws Exception {        // http.formLogin() // 表单方式        http.httpBasic() // HTTP Basic方式                .and()                .authorizeRequests() // 授权配置                .anyRequest()  // 所有请求                .authenticated(); // 都需要认证    }}

3.用户名/密码配置

随机生成的密码,每次启动时都会变。对登录的用户名/密码进行配置,有三种不同的方式:

  • 在 application.properties 中进行配置

  • 通过 Java 代码配置在内存中

  • 通过 Java 从数据库中加载

3.1 配置文件配置用户名/密码

可以直接在配置文件 application.yml文件中配置用户的基本信息:

spring:  security:    user:      name: bos      password: 123

配置完成后,重启项目,就可以使用配置文件中的用户名/密码登录了。

3.2 Java 配置用户名/密码

也可以在 Java 代码中配置用户名密码,首先需要我们创建一个 Spring Security 的配置类,集成自 WebSecurityConfigurerAdapter 类,如下:

@Configurationpublic 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("bos").password("$2a$10$p1H8iWa8I4.CA.7Z8bwLjes91ZpY.rYREGHQEInNtAp4NzL6PLKxi");    }        @Bean    PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }}

这里我们在 configure 方法中配置了两个用户,用户的密码都是加密之后的字符串(明文是 123),从 Spring5 开始,强制要求密码要加密,如果非不想加密,可以使用一个过期的 PasswordEncoder 的实例 NoOpPasswordEncoder,但是不建议这么做,毕竟不安全。

Spring Security 中提供了 BCryptPasswordEncoder 密码编码工具,可以非常方便的实现密码的加密加盐,相同明文加密出来的结果总是不同,这样就不需要用户去额外保存的字段了,这一点比 Shiro 要方便很多。

3.3 从数据库加载用户名/密码

3.3.1 创建SpringSecurityConfig类

@Configuration@EnableWebSecuritypublic class SpringSecurityConfig extends WebSecurityConfigurerAdapter {     @Autowired    private MyUserService myUserService;     @Override    protected void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()                .antMatchers("/authentication/*","/login") // 不需要登录就可以访问                .permitAll()                .antMatchers("/user/**").hasAnyRole("USER") // 需要具有ROLE_USER角色才能访问                .antMatchers("/admin/**").hasAnyRole("ADMIN") // 需要具有ROLE_ADMIN角色才能访问                .anyRequest().authenticated()                .and()                    .formLogin()                    .loginPage("/authentication/login") // 设置登录页面                    .loginProcessingUrl("/authentication/form")                    .defaultSuccessUrl("/user/index"); // 设置默认登录成功后跳转的页面        }     // 密码加密方式    @Bean    public PasswordEncoder passwordEncoder(){        return new BCryptPasswordEncoder();    }    // 重写方法,自定义用户    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {//        auth.inMemoryAuthentication().withUser("lzc").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN","USER");//        auth.inMemoryAuthentication().withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");        auth.userDetailsService(myUserService); // 注入MyUserService,这样SpringSecurity会调用里面的loadUserByUsername(String s)    }}

3.3.2 MyUserService

这个类实现了UserDetailsService接口,里面有一个loadUserByUsername(String s)方法,这个方法返回一个UserDetails ,UserDetails 是一个接口,而org.springframework.security.core.userdetails.User实现了UserDetails,因此这里我们可以直接使用SpringSecurity提供的User对象,当然如果不想使用SpringSecurity提供的User对象,我们也可以自己编写一个实现UserDetails接口的对象。

@Componentpublic class MyUserService implements UserDetailsService {    @Override    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {        //  省略从数据库中获取用户信息的过程...        //  通过用户名s去数据库里查找用户以及用户权限        //  然后返回User对象,注意,这里的User对象是SpringSecurity提供的           return new User(s,new BCryptPasswordEncoder().encode("123456"),AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER"));    }}

4.自定义用户认证

对于登录接口,登录成功后的响应,登录失败后的响应,我们都可以在 WebSecurityConfigurerAdapter 的实现类中进行配置。例如下面这样:

@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {    @Autowired    VerifyCodeFilter verifyCodeFilter;  //验证码过滤器,继承OncePerRequestFilter,保证过滤器每次请求只会被调用一次    @Override    protected void configure(HttpSecurity http) throws Exception {        http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);        http        .authorizeRequests()//开启登录配置        .antMatchers("/hello").hasRole("admin")//表示访问 /hello 这个接口,需要具备 admin 这个角色        .anyRequest().authenticated()//表示剩余的其他接口,登录之后就能访问        .and()        .formLogin()        //定义登录页面,未登录时,访问一个需要登录之后才能访问的接口,会自动跳转到该页面        .loginPage("/login_p")        //登录处理接口        .loginProcessingUrl("/doLogin")        //定义登录时,用户名的 key,默认为 username        .usernameParameter("uname")        //定义登录时,用户密码的 key,默认为 password        .passwordParameter("passwd")        //登录成功的处理器        .successHandler(new AuthenticationSuccessHandler() {            @Override            public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {                    resp.setContentType("application/json;charset=utf-8");                    PrintWriter out = resp.getWriter();                    out.write("success");                    out.flush();                }            })            .failureHandler(new AuthenticationFailureHandler() {                @Override                public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {                    resp.setContentType("application/json;charset=utf-8");                    PrintWriter out = resp.getWriter();                    out.write("fail");                    out.flush();                }            })            .permitAll()//和表单登录相关的接口统统都直接通过            .and()            .logout()            .logoutUrl("/logout")            .logoutSuccessHandler(new LogoutSuccessHandler() {                @Override                public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {                    resp.setContentType("application/json;charset=utf-8");                    PrintWriter out = resp.getWriter();                    out.write("logout success");                    out.flush();                }            })            .permitAll()            .and()            .httpBasic()            .and()            .csrf().disable();    }}

我们可以在 successHandler 方法中,配置登录成功的回调,如果是前后端分离开发的话,登录成功后返回 JSON 即可,同理,failureHandler 方法中配置登录失败的回调,logoutSuccessHandler 中则配置注销成功的回调。

5.忽略拦截

如果某一个请求地址不需要拦截的话,有两种方式实现:

  • 设置该地址匿名访问

  • 直接过滤掉该地址,即该地址不走 Spring Security 过滤器链

推荐使用第二种方案,配置如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/vercode");
}
}

6.基本原理

代码的执行过程可以简化为下图表示:

2ecf11ceff6bc0363841d446984b161c.png

如上图所示,Spring Security包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。其中UsernamePasswordAuthenticationFilter过滤器用于处理基于表单方式的登录认证,而BasicAuthenticationFilter用于处理基于HTTP Basic方式的登录验证,后面还可能包含一系列别的过滤器(可以通过相应配置开启)。在过滤器链的末尾是一个名为FilterSecurityInterceptor的拦截器,用于判断当前请求身份认证是否成功,是否有相应的权限,当身份认证失败或者权限不足的时候便会抛出相应的异常。ExceptionTranslateFilter捕获并处理,所以我们在ExceptionTranslateFilter过滤器用于处理了FilterSecurityInterceptor抛出的异常并进行处理,比如需要身份认证时将请求重定向到相应的认证页面,当认证失败或者权限不足时返回相应的提示信息。

Spring Security 可以结合 OAuth2 ,玩出更多的花样出来

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:81
帖子:4969
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP