SpringSecurity简单入门

175 阅读11分钟

简介

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。

Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,SpringSecurity安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。

从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。

怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生,而Spring Security就是其中的一种。

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和 用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

一.SpringBoot整合Security

1.基本配置

首先导入依赖,SpringSecurity依赖于Spring框架,与之进行对比的是独立存在的shiro框架

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置文件如下:只需要改端口号,另外的配置可以后面用到了再加

#设置端口号
server.port=8065
#SpringSecurity配置(更改默认的登录拦截页面的账号与密码)
spring.security.user.name=javaboy
spring.security.user.password=123
#设置静态资源的路径
spring.web.resources.static-locations=classpath:/static/

2.认证与授权

这里给上核心代码:下面将介绍这些代码的作用

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder(){
        //设置密码不加密,security在使用自定义页面时,密码默认也是需要加密的
        return NoOpPasswordEncoder.getInstance();  //

    }

    @Override
    //认证,用户名,密码,权限
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{

        //如果需要配置多个用户,可以用and相连  .and().withUser...........
        auth.inMemoryAuthentication()
                .withUser("javaboy").password("123").roles("admin")
                .and()
                .withUser("xt").password("123").roles("user");
    }

    @Override
    public void configure(WebSecurity webSecurity) throws Exception{
        //配置自定义的登录界面的资源,需要配置文件配置静态资源路径
        webSecurity.ignoring().antMatchers("/js/**","/css/**","/images/**");

    }

    //配置自定义的登录页面
    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception{

        httpSecurity.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")//配置权限
                .antMatchers("/user/**").hasRole("user") //配置权限
                .anyRequest().authenticated();

        httpSecurity.formLogin()
                .loginPage("/login.html")  //配置登录时的登录页面
                .loginProcessingUrl("/login")  //配置登录时使用的登录接口
                .usernameParameter("username")
                .passwordParameter("password")

//                .successForwardUrl("/index")
                //登录成功的处理
                .successHandler((httpServletRequest, httpServletResponse, authentication) -> {
                    Object principal = authentication.getPrincipal();
                    httpServletResponse.setContentType("application/json;charset=utf-8");
                    PrintWriter out = httpServletResponse.getWriter();
                    //压面上展示principal的所有内容
                    out.write(new ObjectMapper().writeValueAsString(principal));
                    out.write("123456");
                    out.flush();
                    out.close();

                })
                .permitAll()  //允许
                .and()
                .csrf().disable();
//                .exceptionHandling()
//                .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
//                    httpServletResponse.setContentType("application/json;charset=utf-8");
//                    PrintWriter writer = httpServletResponse.getWriter();
//                    writer.write("尚未登录!");
//                    writer.flush();
//                    writer.close();
//                });


    }

    /**
     * 角色继承,admin有user的所有功能
     */
    @Bean
    RoleHierarchy roleHierarchy(){
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy("ROLE_admin > ROLE_user");
        return hierarchy;
    }

}

(1)认证

其中,认证的代码如下,security在不使用自定义登陆界面时,访问任何路径都会被拦截,并跳转到security的自带的登陆界面,且密码会自动加密并在控制台输出,或者可在配置文件配置用户名与密码。在使用自定义登陆时,默认也是需要将密码进行加密,并且内置了多种加密方式

	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{	
		//在内存中定义,也可以在jdbc中去拿....
        //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
        //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
        //spring security 官方推荐的是使用bcrypt加密方式。
        //可以配置密码不加密

        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("qingfeng").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
                .and()
                .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
                .and()
                .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2");
    }

当然,也可以不使用加密,只需要重写passwordEncoder()方法,并将其作为bean组件注入即可

@Bean
    PasswordEncoder passwordEncoder(){
        //设置密码不加密,security在使用自定义页面时,密码默认也是需要加密的
        return NoOpPasswordEncoder.getInstance();  //

    }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{

     //如果需要配置多个用户,可以用and相连  .and().withUser...........
     auth.inMemoryAuthentication()
           .withUser("java").password("123").roles("admin")
           .and()
           .withUser("xt").password("123").roles("user");
}

在自定义登陆页面时,可以配置指定的自定义登陆界面所需要的各种资源

 @Override
    public void configure(WebSecurity webSecurity) throws Exception{
        //配置自定义的登录界面的资源
        webSecurity.ignoring().antMatchers("/js/**","/css/**","/images/**");

    }

(2)授权

配置授权时,使用HttpSecurity,需要配置的首先是授权的url和其所需要的权限,并可以设置任何请求都被拦截监控

可以用  .antMatchers("/").permitAll  来设置登陆页面不需要被验证,也就是根页面
 //配置自定义的登录页面
    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception{
    
        httpSecurity.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")//配置权限
                .antMatchers("/user/**").hasRole("user") //配置权限
            	//设置任何请求都需要被验证
                .anyRequest().authenticated();
        
        httpSecurity.formLogin()   //设置为表单登录
                .loginPage("/login.html")  //配置登录时的登录页面
                .loginProcessingUrl("/login")  //配置登录时使用的登录接口
                .successForwardUrl("/index")   //配置登陆成功跳转的页面
                .failureForwardUrl("/error")  //设置登陆失败跳转的页面
                .permitAll()  //设置允许所有的这些跳转
                .and()
                .csrf().disable();  //跨域请求伪造
        
    }

其中,从 Spring Security4 开始 CSRF 防护默认开启。默认会拦截请求。进行 CSRF 处理。CSRF 为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf 值为 token(token 在服务端产生)的内容,如果token 和服务端的 token 匹配成功,则正常访问。

这里的匹配规则我们采用了 Ant 风格的路径匹配符,Ant 风格的路径匹配符在 Spring 家族中使用非常广泛,它的匹配规则也非常简单:

通配符含义
**匹配多层路径
*匹配一层路径
?匹配任意单个字符

上面配置的含义是:

  1. 如果请求路径满足 /admin/** 格式,则用户需要具备 admin 角色。
  2. 如果请求路径满足 /user/** 格式,则用户需要具备 user 角色。
  3. 剩余的其他格式的请求路径,只需要认证(登录)后就可以访问。

注意代码中配置的三条规则的顺序非常重要,和 Shiro 类似,Spring Security 在匹配的时候也是按照从上往下的顺序来匹配,一旦匹配到了就不继续匹配了,所以拦截规则的顺序不能写错


还可以通过重写 WebSecurityConfigurerAdapter 中的 userDetailsService 方法来提供一个 UserDetailService 实例进而配置多个用户:

@Bean
protected UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("student").password("123").roles("admin").build());
    manager.createUser(User.withUsername("xiaoming").password("123").roles("user").build());
    return manager;
}

具体的复杂认证如下,可以使用successHandler与failureHandler来处理登陆时的失败与成功,

注意:默认情况下,所有未登陆的访问请求都将被拦截,并将跳转到登陆页面

httpSecurity.formLogin()
                .loginPage("/login.html")  //配置登录时的登录页面
                .loginProcessingUrl("/login")  //配置登录时使用的登录接口
                .usernameParameter("username")  //配置用户名的参数
                .passwordParameter("password")  //配置登陆密码,要与表单中的name一致

				
                .successHandler((httpServletRequest, httpServletResponse, authentication) -> {
                    Object principal = authentication.getPrincipal();
                    httpServletResponse.setContentType("application/json;charset=utf-8");
                    PrintWriter out = httpServletResponse.getWriter();
                    out.write(new ObjectMapper().writeValueAsString(principal));
                    out.write("123456");
                    out.flush();
                    out.close();

                })
                .permitAll()
                .and()
                .csrf().disable();
//                .exceptionHandling()
//                .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
//                    httpServletResponse.setContentType("application/json;charset=utf-8");
//                    PrintWriter writer = httpServletResponse.getWriter();
//                    writer.write("尚未登录!");
//                    writer.flush();
//                    writer.close();
//                });

非前后端分离下:

登录成功回调

  1. defaultSuccessUrl 有一个重载的方法,我们先说一个参数的 defaultSuccessUrl 方法。如果我们在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到 /index,如果你是在浏览器中输入了其他地址,例如 http://localhost:8080/hello,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到 /index ,而是来到 /hello 页面。
  2. defaultSuccessUrl 还有一个重载的方法,第二个参数如果不设置默认为 false,也就是我们上面的的情况,如果手动设置第二个参数为 true,则 defaultSuccessUrl 的效果和 successForwardUrl 一致。
  3. successForwardUrl 表示不管你是从哪里来的,登录后一律跳转到 successForwardUrl 指定的地址。例如 successForwardUrl 指定的地址为 /index ,你在浏览器地址栏输入 http://localhost:8080/hello,结果因为没有登录,重定向到登录页面,当你登录成功之后,就会服务端跳转到 /index 页面;或者你直接就在浏览器输入了登录页面地址,登录成功后也是来到 /index。

登录失败回调

与登录成功相似,登录失败也是有两个方法:

  • failureForwardUrl
  • failureUrl

这两个方法在设置的时候也是设置一个即可。failureForwardUrl 是登录失败之后会发生服务端跳转,failureUrl 则在登录失败之后,会发生重定向。

(3)登录处理(成功/失败/注销):

【1】登录成功:successHandler

该方法有三个参数:

  • req:相当于HttpServletRequest、

  • res:相当于HttpServletRespose

  • authentication:这里保存了我们登录后的用户信息

通常进行如下配置

principal:用户信息,包括用户名,密码所持有权限,账户是否过期,锁定等配置

PrintWriter:获取页面的输出对象,可以直接将信息输出到页面上,需要刷掉缓存与关闭

.successHandler((httpServletRequest, httpServletResponse, authentication) -> {
    			//获取用户信息
                    Object principal = authentication.getPrincipal();
                    httpServletResponse.setContentType("application/json;charset=utf-8");
                    PrintWriter out = httpServletResponse.getWriter();
                    out.write(new ObjectMapper().writeValueAsString(principal));
                    out.write("123456");
                    out.flush();
                    out.close();

})

【2】登录失败

该方法有三个参数

req:相当与HttpServletRequest

res:相当与HttpServletRespose

e:这里保存了我们登录失败的原因

其中,异常种类有如下几种:

  • LockedException 账户锁定

  • CredentialsExpiredException 密码过期

  • AccountExpiredException 账户过期

  • DisabledException 账户被禁止

  • BadCredentialsException 用户名或者密码错误

.failureHandler((req, res, e) -> {
                    res.setContentType("application/json;charset=utf-8");
                    PrintWriter out = res.getWriter();
                    out.write(e.getMessage());
                    out.flush();
                    out.close();
                })

【3】未认证登录处理方法

spring security默认情况下,如果认证不成功,直接重定向到登录页面。 但是项目中,我们有的时候不需要这样,我们需要在前端进行判断 ,然后再决定进行其他的处理,那我们就可以用authenticationEntryPoint这个接口进行自定义了,取消它的默认重定向行为

该方法有三个参数:

  • req:相当与HttpServletRequest

  • res:相当与HttpServletRespose

  • authException:指的就是我们未认证的exception

	.exceptionHandling()  //异常处理
        //取消重定向,并给出操作
        .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
               httpServletResponse.setContentType("application/json;charset=utf-8");
               PrintWriter writer = httpServletResponse.getWriter();
               writer.write("尚未登录!");
               writer.flush();
               writer.close();
         });

【4】登录注销

注销登录的默认接口是 /logout,我们也可以配置。

.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))
.logoutSuccessUrl("/index")
.deleteCookies()
.clearAuthentication(true)
.invalidateHttpSession(true)
.permitAll()
.and()

注销登录的配置如下

  1. 默认注销的 URL 是 /logout,是一个 GET 请求,我们可以通过 logoutUrl 方法来修改默认的注销 URL。

  2. logoutRequestMatcher 方法不仅可以修改注销 URL,还可以修改请求方式,实际项目中,这个方法和 logoutUrl 任意设置一个即可。

  3. logoutSuccessUrl 表示注销成功后要跳转的页面。

  4. deleteCookies 用来清除 cookie。

  5. clearAuthentication 和 invalidateHttpSession 分别表示清除认证信息和使 HttpSession 失效,默认可以不用配置,默认就会清除。

注销登陆的处理如下:

		.logoutSuccessHandler((req, res, authentication) -> {
                    res.setContentType("application/json;charset=utf-8");
                    PrintWriter out = res.getWriter();
                    out.write("注销成功");
                    out.flush();
                    out.close();
                })

实战

登录页面,其请求名必须转到login,请求类型为post

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
​
        <div class="title">登录</div>
        <form action="/login" method="post">
            <div class="input">
                <label for="name">用户名</label>
                <input type="text" name="username" id="name">
            </div>
            <div class="input">
                <label for="pass">密码</label>
                <input type="password" name="password" id="pass">
            </div>
            <div class="button login">
                <button type="submit">
                    <span>登录</span>
                </button>
            </div>
        </form></body>
</html>

接口类

@RestController
public class TestController {
​
    @GetMapping("/hello")
    public String test(){
​
        return "hello";
​
    }
​
    @PostMapping("/index")
    public  String index(){
        return "index";
    }
​
    @GetMapping("/admin/hello")
    public String admin() {
        return "admin";
    }
​
    @GetMapping("/user/hello")
    public String user() {
        return "user";
    }
​
}

测试如下:

首先:页面在被访问时会自动跳转到自定义登陆页面

image.png

在登陆成功后:会出现包括用户名,密码,权限在内的信息

image.png

登录用户为java,只有admin权限

可以访问 /admin/hello路径

当有角色权限继承的时候: 也可以访问到/user/hello页面

  /**
     * 角色继承,admin有user的所有功能
     */
    @Bean
    RoleHierarchy roleHierarchy(){
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy("ROLE_admin > ROLE_user");
        return hierarchy;
    }