【Spring Boot】25.安全

简介

安全是咱们开发中一直须要考虑的问题,例如作身份认证、权限限制等等。市面上比较常见的安全框架有:html

  • shiro
  • spring security

shiro比较简单,容易上手。而spring security功能比较强大,可是也比较难以掌握。springboot集成了spring security,咱们此次来学习spring security的使用。java

spring security

应用程序的两个主要区域是“认证”和“受权”(或者访问控制)。这两个主要区域是Spring Security 的两个目标。git

  • “认证”(Authentication),是创建一个他声明的主体的过程(一个“主体”通常是指用户,设备或一些能够在你的应用程序中执行动做的其余系统)。github

  • “受权”(Authorization)指肯定一个主体是否容许在你的应用程序执行一个动做的过程。为了抵达须要受权的店,主体的身份已经有认证过程创建。web

这个概念是通用的而不仅在Spring Security中。spring

Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型。他能够实现强大的web安全控制。对于安全控制,咱们仅需引入spring-boot-starter-security模块,进行少许的配置,便可实现强大的安全管理。须要注意几个类:数据库

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式

测试使用

搭建基本测试环境

  1. 引入thymeleaf和security场景启动器
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
  1. 编写几个简单的html页面,咱们将其分别放在不一样的模板文件夹子目录user,admin以及super中,预备给咱们的3种不一样的角色访问适用。
++template
----index.html
++++user
------user1.html
------user2.html
------user3.html
++++admin
------admin1.html
------admin2.html
------admin3.html
++++super
------super1.html
------super2.html
------super3.html

每一个html都写一点简单的内容,相似于this is xxxx.html。例如admin/admin3.html的内容以下:浏览器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>admin-3</title>
</head>
<body>
this is admin-3 file.
</body>
</html>

为了方便查看,你也能够将title标签体内容修改成一致的名称,如admin-3安全

  1. 编写controller,对咱们的访问路径进行映射:
package com.example.dweb.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {
    @GetMapping("/")
    public String user1(){
        return "/index";
    }

    @GetMapping("/user/user1")
    public String user1(){
        return "/user/user1";
    }
    @GetMapping("/user/user2")
    public String user2(){
        return "/user/user2";
    }
    @GetMapping("/user/user3")
    public String user3(){
        return "/user/user3";
    }


    @GetMapping("/admin/admin1")
    public String admin1(){
        return "/admin/admin1";
    }
    @GetMapping("/admin/admin2")
    public String admin2(){
        return "/admin/admin2";
    }
    @GetMapping("/admin/admin3")
    public String admin3(){
        return "/admin/admin3";
    }


    @GetMapping("/super/super1")
    public String super1(){
        return "/super/super1";
    }
    @GetMapping("/super/super2")
    public String super2(){
        return "/super/super2";
    }
    @GetMapping("/super/super3")
    public String super3(){
        return "/super/super3";
    }
}
  1. 运行项目,测试咱们对各个页面的访问是否正常。在运行项目以前,先在pom文件中将spring-security场景启动器删除,避免security对咱们进行访问拦截:
<!--<dependency>-->
        <!--<groupId>org.springframework.boot</groupId>-->
        <!--<artifactId>spring-boot-starter-security</artifactId>-->
    <!--</dependency>

默认状况下,springsecurity会生成登陆页面要求用户进行登陆,默认用户名为user,密码为启动项目时控制台info级别打印出的一串uuid,可查看源码了解String password = UUID.randomUUID().toString()springboot

安全配置编写

配置编写过程能够参考官方网站文档:前往,以及springsecurity的官方文档:前往

注意版本号,这里是2.1.x版本的适用文档。

  1. 中止项目,将咱们以前注释掉的springsecutiry场景启动器源码还原.
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  1. 编写配置类config/MySecurityConfig控制请求的访问权限
package com.example.dweb.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");
    }
}

能够看到,咱们定制了以下的访问规则:

  • 对于首页,容许任何人访问
  • /user/** 这样的请求,只有角色为user、admin以及super的人才能访问;
  • /admin/** 这样的请求,角色类型为admin和super的能够访问
  • /super/** 这样的请求,角色类型为super的能够访问

假设认证用户只有这三种角色类型的话,那么super拥有最高的访问权限,admin次之,而user最小。

别忘了为该配置类添加@EnableWebSecurity注解. 接下来咱们启动项目,访问/能够正常访问,可是咱们访问/user/user1等以后会报错:

...
There was an unexpected error (type=Forbidden, status=403).
Access Denied

权限禁止,达成了咱们的目的。

登陆

  1. 接下来咱们经过用户登陆,来实现对不一样角色的访问限制。咱们前面注释掉了super.configure(http);这样的代码,这里是默认的安全配置,其中就指定了默认的登陆界面。咱们能够本身来开启自动配置的登陆功能(http.formLogin();)。看其源码:
/**
	 * Specifies to support form based authentication. If
	 * {@link FormLoginConfigurer#loginPage(String)} is not specified a default login page
	 * will be generated.
	 *
	 * <h2>Example Configurations</h2>
	 *
	 * The most basic configuration defaults to automatically generating a login page at
	 * the URL "/login", redirecting to "/login?error" for authentication failure. The
	 * details of the login page can be found on
	 * {@link FormLoginConfigurer#loginPage(String)}
	 *
	 * <pre>
	 * &#064;Configuration
	 * &#064;EnableWebSecurity
	 * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
	 *
	 * 	&#064;Override
	 * 	protected void configure(HttpSecurity http) throws Exception {
	 * 		http.authorizeRequests().antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin();
	 * 	}
	 *
	 * 	&#064;Override
	 * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	 * 		auth.inMemoryAuthentication().withUser(&quot;user&quot;).password(&quot;password&quot;).roles(&quot;USER&quot;);
	 * 	}
	 * }
	 * </pre>
	 *
	 * The configuration below demonstrates customizing the defaults.
	 *
	 * <pre>
	 * &#064;Configuration
	 * &#064;EnableWebSecurity
	 * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
	 *
	 * 	&#064;Override
	 * 	protected void configure(HttpSecurity http) throws Exception {
	 * 		http.authorizeRequests().antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin()
	 * 				.usernameParameter(&quot;username&quot;) // default is username
	 * 				.passwordParameter(&quot;password&quot;) // default is password
	 * 				.loginPage(&quot;/authentication/login&quot;) // default is /login with an HTTP get
	 * 				.failureUrl(&quot;/authentication/login?failed&quot;) // default is /login?error
	 * 				.loginProcessingUrl(&quot;/authentication/login/process&quot;); // default is /login
	 * 																		// with an HTTP
	 * 																		// post
	 * 	}
	 *
	 * 	&#064;Override
	 * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	 * 		auth.inMemoryAuthentication().withUser(&quot;user&quot;).password(&quot;password&quot;).roles(&quot;USER&quot;);
	 * 	}
	 * }
	 * </pre>
	 *
	 * @see FormLoginConfigurer#loginPage(String)
	 *
	 * @return the {@link FormLoginConfigurer} for further customizations
	 * @throws Exception
	 */
	public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
		return getOrApply(new FormLoginConfigurer<>());
	}

注释已经说明了一切,咱们能够总结一下:

  • /login 请求能够来到登陆页面;
  • 若登陆错误,会重定向到login?error;
  • 其余的咱们能够根据本身的需求查询,还算是比较全面的;
  1. 如今咱们的配置类以下所示:
// super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");

        // open auto login
        http.formLogin();
  1. 启动项目,再次访问受限的页面如/user/user1,这一次,系统将会将页面定向到登陆页面了。如今系统提供的只是一个位于内存中默认用户名为user,密码为一串随即UUID的帐户。显然这对于实际开发没有什么意义,所以咱们通常是链接数据库等进行用户数据对比的。

接下来,咱们能够自定义用户的认证过程,但为了演示方便,咱们就使用内存帐户进行认证了。

  1. 要定制自定义用户认证过程
package com.example.dweb.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");

        // open auto login
        http.formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
        String password = "123456";
        auth.inMemoryAuthentication()
                .withUser("user").password(password).roles("user")
                .and()
                .withUser("admin").password(password).roles("admin")
                .and()
                .withUser("super").password(password).roles("super");

    }

    // use my password encoder, it like original password.
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(rawPassword);
            }
        };
    }

}

咱们定义了3个帐户,其用户名和角色名一致,密码都为123456.

启动项目,咱们试试测试效果。

能够看到,访问结果和咱们预期的同样。

  • 若登陆用户是user,则只能访问/user/**
  • 若登陆用户是admin,则能访问/admin/**以及/user/**;
  • 若登陆帐户是super,则能访问全部页面;

若是你使用的是高版本(5.X)的spring security 可能会遇到java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"这样的错误,解决方案就是使用密码编码器,即PasswordEncorder实例,所以,咱们须要在此处提供PasswordEncorder类型的bean passwordEncorder,spring security提供了很多实例供咱们使用,能够本身去查看,例如BCryptPasswordEncoder等等,不过要注意有很多已经标注了@Deprecated,好比LdapShaPasswordEncoderStandardPasswordEncoder,使用以前参考一下源代码好些。我这里为了偷懒,本身用了一个明文验证的PasswordEncoder。

NoOpPasswordEncoder也是一个spring security 提供的PasswordEncorder,可是请注意,他已经被标注@Deprecated,即不推荐使用的注解。因此若是须要明文验证,本身定义一个PasswordEncoder的bean就能够了。

注销

注销和登陆同样,咱们须要先卡其自动配置的注销功能:

@Override
    protected void configure(HttpSecurity http) throws Exception {
       // super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");

        // open auto login function.
        http.formLogin();
        // open auto logout function.
        http.logout();
    }

查看该方法的源码:

/**
	 * Provides logout support. This is automatically applied when using
	 * {@link WebSecurityConfigurerAdapter}. The default is that accessing the URL
	 * "/logout" will log the user out by invalidating the HTTP Session, cleaning up any
	 * {@link #rememberMe()} authentication that was configured, clearing the
	 * {@link SecurityContextHolder}, and then redirect to "/login?success".
	 *
	 * <h2>Example Custom Configuration</h2>
	 *
	 * The following customization to log out when the URL "/custom-logout" is invoked.
	 * Log out will remove the cookie named "remove", not invalidate the HttpSession,
	 * clear the SecurityContextHolder, and upon completion redirect to "/logout-success".
	 *
	 * <pre>
	 * &#064;Configuration
	 * &#064;EnableWebSecurity
	 * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter {
	 *
	 * 	&#064;Override
	 * 	protected void configure(HttpSecurity http) throws Exception {
	 * 		http.authorizeRequests().antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin()
	 * 				.and()
	 * 				// sample logout customization
	 * 				.logout().deleteCookies(&quot;remove&quot;).invalidateHttpSession(false)
	 * 				.logoutUrl(&quot;/custom-logout&quot;).logoutSuccessUrl(&quot;/logout-success&quot;);
	 * 	}
	 *
	 * 	&#064;Override
	 * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	 * 		auth.inMemoryAuthentication().withUser(&quot;user&quot;).password(&quot;password&quot;).roles(&quot;USER&quot;);
	 * 	}
	 * }
	 * </pre>
	 *
	 * @return the {@link LogoutConfigurer} for further customizations
	 * @throws Exception
	 */
	public LogoutConfigurer<HttpSecurity> logout() throws Exception {
		return getOrApply(new LogoutConfigurer<>());
	}

很是详细,咱们大体能够了解到:

  1. 访问/logout会清空session以及全部的认证信息。
  2. 注销成功后会跳转到页面/login?success

咱们给首页添加一个退出按钮:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
this is index file.

<form th:action="@{/logout}" method="post">
<input type="submit" value="注销" />
</form>
</body>
</html>

运行项目以后咱们登陆以后进入首页,点击退出按钮,发现来到了登陆界面。

这是默认的,咱们也能够定制退出页面的位置,只需以下设置便可。

http.logout().logoutSuccessUrl("/");

点击注销感受仍是没反应(实际上是刷新了一下),由于当前页面和退出页面是同样的。

thymeleaf整合安全模块

咱们有时候有不少需求须要和用户的角色绑定,例如管理员会显示一些多余的菜单等等,有一种解决方案就是经过thymeleaf和spring security的整合模块来完成。

<dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>

官方文档能够查看地址:文档

如今咱们来完善一下以前的注销按钮的问题,他应该出如今已登陆帐户的页面才是,而不该该出如今未登陆的用户的首页。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<div sec:authorize="isAuthenticated()">
    <h1><span sec:authentication="name"></span>,您好,您的角色是<span sec:authentication="principal.authorities"></span></h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="注销" />
    </form>
</div>
<hr>
<div sec:authorize="!isAuthenticated()">
    你好,您当前未登陆,请预先<a th:href="@{/login}">登陆</a>
</div>

<hr>
this is index file.

</body>
</html>

注意sec:authorize以及sec:authentication的区别。

其实该插件基本都是操做一些内置对象,例如authentication等的,所以,有的地方也能够用thymeleaf的基础语法直接访问。 例如如下两端代码输出是同样的:

<h1 th:text="${#authentication.getName()}"></h1>
<h1 sec:authentication="name"></h1>

记住我

记住我功能也是比较经常使用的登陆便利条款,开启记住我只需这样配置:

http.rememberMe();

如今咱们的配置类以下所示:

package com.example.dweb.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");

        // open auto login function.
        http.formLogin();
        // open auto logout function.
        http.logout().logoutSuccessUrl("/");
        // open remember me function.
        http.rememberMe();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
        String password = "123456";
        auth.inMemoryAuthentication()
                .withUser("user").password(password).roles("user")
                .and()
                .withUser("admin").password(password).roles("admin")
                .and()
                .withUser("super").password(password).roles("super");

    }

    // use my password encoder, it like original password.
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(rawPassword);
            }
        };
    }

}

重启项目以后来到登陆页面,会发现自动为咱们添加了带有文本Remember me on this computer的复选框按钮,经过勾选该按钮以后登陆,即使咱们关掉浏览器,访问咱们的网站的时候就无需再次登陆了,至关于记住了咱们的认证信息。

咱们来探究一下其实现原理: 打开浏览器的控制台(我使用的是google),找到application选项卡的Cookies菜单栏,会发现里面有两个数据

  • JSESSIONID
  • remember-me

有效时间14天左右,浏览器经过remember-me和服务器进行交互,检查以后就无需登陆了。

当咱们点击注销按钮,则会删除这个Cookie。

定制登录页

正常状况下咱们确定是不能依靠springboot为咱们提供的login页面的,须要本身定制,定制方式也很简单,在开启登陆功能的地方定制便可。

// open auto login function.
    http.formLogin().loginPage("/login");

在模板目录下放置一个登陆界面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<form method="post" action="@{/userLogin}">
    <input type="text" name="username" value="user"/>
    <input type="text" name="password" value="123456"/>
    <input type="submit" value="login">
</form>

</body>
</html>

IndexController中添加对login的映射:

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

默认状况下post形式的/login表明处理登陆,默认证字段就是username和password,固然,你也能够修改

// open auto login function.
http.formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password");

留意loginPage的源码注释部分:

* If "/authenticate" was passed to this method it update the defaults as shown below:
	 *
	 * <ul>
	 * <li>/authenticate GET - the login form</li>
	 * <li>/authenticate POST - process the credentials and if valid authenticate the user
	 * </li>
	 * <li>/authenticate?error GET - redirect here for failed authentication attempts</li>
	 * <li>/authenticate?logout GET - redirect here after successfully logging out</li>
	 * </ul>

也就说,一旦咱们定制了登陆页面,那么其余的规则也会受到影响,大体就是

  • /page Get 处理前往登陆页面
  • /page post 处理登陆请求
  • /page?success GET 登录成功请求
  • /page?error GET 登录失败请求

固然,咱们也能够修改处理登陆页面的地址:

http.formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password").loginProcessingUrl("/login");

注意是post方式。

一样的,rememberme也支持自定义配置。

// open remember me function.
http.rememberMe().rememberMeParameter("remember-me");

name咱们设置为默认的就好,这样就无需配置了,查看源码能够知道默认是什么:

private static final String DEFAULT_REMEMBER_ME_NAME = "remember-me";

这样,咱们在登录页面添加rememberme按钮

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<form method="post" th:action="@{/login}">
    <input type="text" name="username" value="user"/>
    <input type="text" name="password" value="123456"/>
    <input th:type="checkbox" name="remember-me"> 记住我
    <input type="submit" value="login">
</form>

</body>
</html>

而后测试其效果,应该和默认的登陆界面是如出一辙的。

相关文章
相关标签/搜索