安全是咱们开发中一直须要考虑的问题,例如作身份认证、权限限制等等。市面上比较常见的安全框架有:html
shiro比较简单,容易上手。而spring security功能比较强大,可是也比较难以掌握。springboot集成了spring security,咱们此次来学习spring security的使用。java
应用程序的两个主要区域是“认证”和“受权”(或者访问控制)。这两个主要区域是Spring Security 的两个目标。git
“认证”(Authentication),是创建一个他声明的主体的过程(一个“主体”通常是指用户,设备或一些能够在你的应用程序中执行动做的其余系统)。github
“受权”(Authorization)指肯定一个主体是否容许在你的应用程序执行一个动做的过程。为了抵达须要受权的店,主体的身份已经有认证过程创建。web
这个概念是通用的而不仅在Spring Security中。spring
Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型。他能够实现强大的web安全控制。对于安全控制,咱们仅需引入spring-boot-starter-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>
++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安全
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"; } }
<!--<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版本的适用文档。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
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"); } }
能够看到,咱们定制了以下的访问规则:
假设认证用户只有这三种角色类型的话,那么super拥有最高的访问权限,admin次之,而user最小。
别忘了为该配置类添加@EnableWebSecurity注解. 接下来咱们启动项目,访问/能够正常访问,可是咱们访问/user/user1等以后会报错:
... There was an unexpected error (type=Forbidden, status=403). Access Denied
权限禁止,达成了咱们的目的。
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> * @Configuration * @EnableWebSecurity * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { * * @Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin(); * } * * @Override * protected void configure(AuthenticationManagerBuilder auth) throws Exception { * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); * } * } * </pre> * * The configuration below demonstrates customizing the defaults. * * <pre> * @Configuration * @EnableWebSecurity * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { * * @Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .usernameParameter("username") // default is username * .passwordParameter("password") // default is password * .loginPage("/authentication/login") // default is /login with an HTTP get * .failureUrl("/authentication/login?failed") // default is /login?error * .loginProcessingUrl("/authentication/login/process"); // default is /login * // with an HTTP * // post * } * * @Override * protected void configure(AuthenticationManagerBuilder auth) throws Exception { * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); * } * } * </pre> * * @see FormLoginConfigurer#loginPage(String) * * @return the {@link FormLoginConfigurer} for further customizations * @throws Exception */ public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); }
注释已经说明了一切,咱们能够总结一下:
// 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();
接下来,咱们能够自定义用户的认证过程,但为了演示方便,咱们就使用内存帐户进行认证了。
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/**
/admin/**
以及/user/**
;若是你使用的是高版本(5.X)的spring security 可能会遇到java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
这样的错误,解决方案就是使用密码编码器,即PasswordEncorder实例,所以,咱们须要在此处提供PasswordEncorder类型的bean passwordEncorder,spring security提供了很多实例供咱们使用,能够本身去查看,例如BCryptPasswordEncoder
等等,不过要注意有很多已经标注了@Deprecated
,好比LdapShaPasswordEncoder
、StandardPasswordEncoder
,使用以前参考一下源代码好些。我这里为了偷懒,本身用了一个明文验证的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> * @Configuration * @EnableWebSecurity * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter { * * @Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .and() * // sample logout customization * .logout().deleteCookies("remove").invalidateHttpSession(false) * .logoutUrl("/custom-logout").logoutSuccessUrl("/logout-success"); * } * * @Override * protected void configure(AuthenticationManagerBuilder auth) throws Exception { * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); * } * } * </pre> * * @return the {@link LogoutConfigurer} for further customizations * @throws Exception */ public LogoutConfigurer<HttpSecurity> logout() throws Exception { return getOrApply(new LogoutConfigurer<>()); }
很是详细,咱们大体能够了解到:
咱们给首页添加一个退出按钮:
<!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和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菜单栏,会发现里面有两个数据
有效时间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>
也就说,一旦咱们定制了登陆页面,那么其余的规则也会受到影响,大体就是
固然,咱们也能够修改处理登陆页面的地址:
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>
而后测试其效果,应该和默认的登陆界面是如出一辙的。