SpringBoot + Spring Security 学习笔记(一)自定义基本使用及个性化登录配置
,,,
springsecurity 核心功能:
- 认证(你是谁)
- 授权(你能干什么)
- 攻击防护(防止伪造身份)
简单的开始
pom 依赖
<?xml version="1.0" encoding="utf-8"?> <project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://maven.apache.org/pom/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelversion>4.0.0</modelversion> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version>2.1.4.release</version> <relativepath/> <!-- lookup parent from repository --> </parent> <groupid>org.woodwhale.king</groupid> <artifactid>security-demo</artifactid> <version>1.0.0</version> <name>security-demo</name> <description>spring-security-demo project for spring boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <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> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-devtools</artifactid> <scope>runtime</scope> </dependency> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> <scope>runtime</scope> </dependency> <dependency> <groupid>org.projectlombok</groupid> <artifactid>lombok</artifactid> <optional>true</optional> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> <scope>test</scope> </dependency> <dependency> <groupid>org.springframework.security</groupid> <artifactid>spring-security-test</artifactid> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> </plugin> </plugins> </build> </project>
编写一个最简单的用户 controller
import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.restcontroller; @restcontroller @requestmapping("/user") public class usercontroller { @getmapping public string getusers() { return "hello spring security"; } }
application.yml 配置ip 和端口
server: address: 127.0.0.1 port: 8081 logging: level: org.woodwhale.king: debug
浏览器访问http://127.0.0.1:8081/user,浏览器被自动重定向到了登录的界面:
这个/login
访问路径在程序中没有任何的显示代码编写,为什么会出现这样的界面呢,当前界面中的ui 都是哪里来的呢?
当然是 spring-security 进行了默认控制,从启动日志中,可以看到一串用户名默认为user
的默认密码:
登录成功之后,可以正常访问服务资源了。
自定义默认用户名和密码
在配置文件配置用户名和密码:
spring: security: user: name: "admin" password: "admin"
关闭默认的安全访问控制
旧版的 spring security 关闭默认安全访问控制,只需要在配置文件中关闭即可:
security.basic.enabled = false
新版本 spring-boot2.xx(spring-security5.x) 的不再提供上述配置了:
方法1: 将 security 包从项目依赖中去除。
方法2:将org.springframework.boot.autoconfigure.security.servlet.securityautoconfiguration
不注入spring中:
import org.springframework.boot.springapplication; import org.springframework.boot.autoconfigure.enableautoconfiguration; import org.springframework.boot.autoconfigure.springbootapplication; import org.springframework.boot.autoconfigure.security.servlet.securityautoconfiguration; @springbootapplication @enableautoconfiguration(exclude = {securityautoconfiguration.class}) public class securitydemoapplication { public static void main(string[] args) { springapplication.run(securitydemoapplication.class, args); } }
方法3:己实现一个配置类继承自websecurityconfigureradapter
,并重写configure(httpsecurity http)
方法:
import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; 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.core.userdetails.userdetailsservice; @configuration @enablewebsecurity public class websecurityconfig extends websecurityconfigureradapter { @override protected void configure(httpsecurity http) throws exception { http.authorizerequests().antmatchers("/**").permitall(); } /** * 配置一个userdetailsservice bean * 不再生成默认security.user用户 */ @bean @override protected userdetailsservice userdetailsservice() { return super.userdetailsservice(); } }
注意:websecurityconfigureradapter
是一个适配器类,所以为了使自定义的配置类见名知义,所以写成了websecurityconfig
。同时增加了@enablewebsecurity
注解到了 spring security 中。
自定义用户认证
安全认证配置注意事项
springsucrity 的自定义用户认证配置的核心均在上述的websecurityconfigureradapter
类中,用户想要个性化的用户认证逻辑,就需要自己写一个自定义的配置类,适配到 spring security 中:
注意:如果配置了两个以上的自定义实现类,那么就会报websecurityconfigurers
不唯一的错误:java.lang.illegalstateexception: @order on websecurityconfigurers must be unique.
@configuration @enablewebsecurity public class browersecurityconfig extends websecurityconfigureradapter { @override protected void configure(httpsecurity http) throws exception { http.formlogin() // 定义当需要提交表单进行用户登录时候,转到的登录页面。 .and() .authorizerequests() // 定义哪些url需要被保护、哪些不需要被保护 .anyrequest() // 任何请求,登录后可以访问 .authenticated(); } }
自定义用户名和密码
密码加密注意事项
将用户名密码设置到内存中,用户登录的时候会校验内存中配置的用户名和密码:
在旧版本的 spring security 中,在上述自定义的browersecurityconfig
中配置如下代码即可:
@override protected void configure(authenticationmanagerbuilder auth) throws exception { auth.inmemoryauthentication().withuser("admin").password("admin").roles("admin"); }
但是在新版本中,启动运行都没有问题,一旦用户正确登录的时候,会报异常:
java.lang.illegalargumentexception: there is no passwordencoder mapped for the id "null"
因为在 spring security 5.0 中新增了多种加密方式,也改变了密码的格式。官方文档说明:password storage format
上面这段话的意思是,现在新的 spring security 中对密码的存储格式是"{id}……"
。前面的 id
是加密方式,id 可以是bcrypt
、sha256
等,后面紧跟着是使用这种加密类型进行加密后的密码。
因此,程序接收到内存或者数据库查询到的密码时,首先查找被{}
包括起来的id
,以确定后面的密码是被什么加密类型方式进行加密的,如果找不到就认为 id 是 null。这也就是为什么程序会报错:there is no passwordencoder mapped for the id "null"
。官方文档举的例子中是各种加密方式针对同一密码加密后的存储形式,原始密码都是"password"。
密码加密
要想我们的项目还能够正常登陆,需要将前端传过来的密码进行某种方式加密,官方推荐的是使用bcrypt
加密方式(不用用户使用相同原密码生成的密文是不同的),因此需要在 configure 方法里面指定一下:
@override protected void configure(authenticationmanagerbuilder auth) throws exception { // auth.inmemoryauthentication().withuser("admin").password("admin").roles("admin"); auth.inmemoryauthentication() .passwordencoder(new bcryptpasswordencoder()) .withuser("admin") .password(new bcryptpasswordencoder().encode("admin")) .roles("admin"); }
当然还有一种方法,将passwordencoder
配置抽离出来:
@bean public bcryptpasswordencoder passwordencoder() { return new bcryptpasswordencoder(); }
自定义到内存
@override protected void configure(authenticationmanagerbuilder auth) throws exception { auth.inmemoryauthentication() .withuser("admin") .password(new bcryptpasswordencoder().encode("admin")) .roles("admin"); }
自定义到代码
这里还有一种更优雅的方法,实现org.springframework.security.core.userdetails.userdetailsservice
接口,重载loaduserbyusername(string username)
方法,当用户登录时,会调用userdetailsservice
接口的loaduserbyusername()
来校验用户的合法性(密码和权限)。
这种方法为之后结合数据库或者jwt动态校验打下技术可行性基础。
@service public class myuserdetailsservice implements userdetailsservice { @override public userdetails loaduserbyusername(string username) throws usernamenotfoundexception { collection<grantedauthority> authorities = new arraylist<>(); authorities.add(new simplegrantedauthority("admin")); return new user("root", new bcryptpasswordencoder().encode("root"), authorities); } }
当然,"自定义到内存"中的配置文件中的configure(authenticationmanagerbuilder auth)
配置就不需要再配置一遍了。
注意:对于返回的userdetails
实现类,可以使用框架自己的 user,也可以自己实现一个 userdetails 实现类,其中密码和权限都应该从数据库中读取出来,而不是写死在代码里。
最佳实践
将加密类型抽离出来,实现userdetailsservice
接口,将两者注入到authenticationmanagerbuilder
中:
@configuration @enablewebsecurity public class websecurityconfig extends websecurityconfigureradapter { @autowired private userdetailsservice userdetailsservice; @bean public bcryptpasswordencoder passwordencoder() { return new bcryptpasswordencoder(); } @override protected void configure(authenticationmanagerbuilder auth) throws exception { auth.userdetailsservice(userdetailsservice) .passwordencoder(passwordencoder()); } }
userdetailsservice
接口实现类:
import java.util.arraylist; import java.util.collection; import org.springframework.security.core.grantedauthority; import org.springframework.security.core.authority.simplegrantedauthority; import org.springframework.security.core.userdetails.user; import org.springframework.security.core.userdetails.userdetails; import org.springframework.security.core.userdetails.userdetailsservice; import org.springframework.security.core.userdetails.usernamenotfoundexception; import org.springframework.security.crypto.bcrypt.bcryptpasswordencoder; import org.springframework.stereotype.service; @service public class myuserdetailsservice implements userdetailsservice { @override public userdetails loaduserbyusername(string username) throws usernamenotfoundexception { collection<grantedauthority> authorities = new arraylist<>(); authorities.add(new simplegrantedauthority("admin")); return new user("root", new bcryptpasswordencoder().encode("root"), authorities); } }
这里的 user 对象是框架提供的一个用户对象,注意包名是:org.springframework.security.core.userdetails.user
,里面的属性中最核心的就是password
,username
和authorities
。
自定义安全认证配置
配置自定义的登录页面:
@override protected void configure(httpsecurity http) throws exception { http.formlogin() // 定义当需要用户登录时候,转到的登录页面。 .loginpage("/login") // 设置登录页面 .loginprocessingurl("/user/login") // 自定义的登录接口 .defaultsuccessurl("/home").permitall() // 登录成功之后,默认跳转的页面 .and().authorizerequests() // 定义哪些url需要被保护、哪些不需要被保护 .antmatchers("/", "/index","/user/login").permitall() // 设置所有人都可以访问登录页面 .anyrequest().authenticated() // 任何请求,登录后可以访问 .and().csrf().disable(); // 关闭csrf防护 }
从上述配置中,可以看出用可以所有访客均可以*登录/
和/index
进行资源访问,同时配置了一个登录的接口/lgoin
,使用mvc做了视图映射(映射到模板文件目录中的login.html
),controller 映射代码太简单就不赘述了,当用户成功登录之后,页面会自动跳转至/home
页面。
上述图片中的配置有点小小缺陷,当去掉
.loginprocessurl()
的配置的时候,登录完毕,浏览器会一直重定向,直至报重定向失败。因为登录成功的 url 没有配置成所有人均可以访问,因此造成了死循环的结果。因此,配置了登录界面就需要配置任意可访问:
.antmatchers("/user/login").permitall()
login.html
代码:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>登录页面</title> </head> <body> <h2>自定义登录页面</h2> <form action="/user/login" method="post"> <table> <tr> <td>用户名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="password"></td> </tr> <tr> <td colspan="2"><button type="submit">登录</button></td> </tr> </table> </form> </body> </html>
静态资源忽略配置
上述配置用户认证过程中,会发现资源文件也被安全框架挡在了外面,因此需要进行安全配置:
@override public void configure(websecurity web) throws exception { web.ignoring().antmatchers("/webjars/**/*", "/**/*.css", "/**/*.js"); }
现在前端框架的静态资源完全可以通过webjars
统一管理,因此注意配置/webjars/**/*
。
处理不同类型的请求
前后端分离的系统中,一般后端仅提供接口 json 格式的数据,以供前端自行调用。刚才那样,调用了被保护的接口,直接进行了页面的跳转,在web端还可以接受,但是在 app 端就不行了, 所以我们还需要做进一步的处理。
这里做一下简单的思路整理
这里提供一种思路,核心在于运用安全框架的:requestcache
和redirectstrategy
import java.io.ioexception; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.http.httpstatus; import org.springframework.security.web.defaultredirectstrategy; import org.springframework.security.web.redirectstrategy; import org.springframework.security.web.savedrequest.httpsessionrequestcache; import org.springframework.security.web.savedrequest.requestcache; import org.springframework.security.web.savedrequest.savedrequest; import org.springframework.util.stringutils; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.responsestatus; import org.springframework.web.bind.annotation.restcontroller; import lombok.extern.slf4j.slf4j; @slf4j @restcontroller public class browsersecuritycontroller { // 原请求信息的缓存及恢复 private requestcache requestcache = new httpsessionrequestcache(); // 用于重定向 private redirectstrategy redirectstrategy = new defaultredirectstrategy(); /** * 当需要身份认证的时候,跳转过来 * @param request * @param response * @return */ @requestmapping("/authentication/require") @responsestatus(code = httpstatus.unauthorized) public string requireauthenication(httpservletrequest request, httpservletresponse response) throws ioexception { savedrequest savedrequest = requestcache.getrequest(request, response); if (savedrequest != null) { string targeturl = savedrequest.getredirecturl(); log.info("引发跳转的请求是:" + targeturl); if (stringutils.endswithignorecase(targeturl, ".html")) { redirectstrategy.sendredirect(request, response, "/login.html"); } } return "访问的服务需要身份认证,请引导用户到登录页"; } }
注意:这个/authentication/require
需要配置到安全认证配置:配置成默认登录界面,并设置成任何人均可以访问,并且这个重定向的页面可以设计成配置,从配置文件中读取。
自定义处理登录成功/失败
在前后端分离的情况下,我们登录成功了可能需要向前端返回用户的个人信息,而不是直接进行跳转。登录失败也是同样的道理。这里涉及到了 spring security 中的两个接口authenticationsuccesshandler
和authenticationfailurehandler
。自定义这两个接口的实现,并进行相应的配置就可以了。 当然框架是有默认的实现类的,我们可以继承这个实现类再来自定义自己的业务:
成功登录处理类
import java.io.ioexception; import javax.servlet.servletexception; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.beans.factory.annotation.autowired; import org.springframework.security.core.authentication; import org.springframework.security.web.authentication.simpleurlauthenticationsuccesshandler; import org.springframework.stereotype.component; import com.fasterxml.jackson.databind.objectmapper; import lombok.extern.slf4j.slf4j; @slf4j @component("myauthenctiationsuccesshandler") public class myauthenctiationsuccesshandler extends simpleurlauthenticationsuccesshandler { @autowired private objectmapper objectmapper; @override public void onauthenticationsuccess(httpservletrequest request, httpservletresponse response, authentication authentication) throws ioexception, servletexception { log.info("登录成功"); response.setcontenttype("application/json;charset=utf-8"); response.getwriter().write(objectmapper.writevalueasstring(authentication)); } }
成功登录之后,通过 response 返回一个 json 字符串回去。这个方法中的第三个参数authentication
,它里面包含了登录后的用户信息(userdetails),session 的信息,登录信息等。
登录成功之后的响应json:
{ "authorities": [ { "authority": "role_admin" } ], "details": { "remoteaddress": "127.0.0.1", "sessionid": "8bfa4f61a7cea774c00f616aae8c307c" }, "authenticated": true, "principal": { "password": null, "username": "admin", "authorities": [ { "authority": "role_admin" } ], "accountnonexpired": true, "accountnonlocked": true, "credentialsnonexpired": true, "enabled": true }, "credentials": null, "name": "admin" }
这里有个细节需要注意:
principal
中有个权限数组集合authorities
,里面的权限值是:role_admin
,而自定义的安全认证配置中配置的是:admin
,所以role_
前缀是框架自己加的,后期取出权限集合的时候需要注意这个细节,以取决于判断是否有权限是使用字符串的包含关系还是等值关系。
登录失败处理类
import java.io.ioexception; import javax.servlet.servletexception; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.beans.factory.annotation.autowired; import org.springframework.http.httpstatus; import org.springframework.security.core.authenticationexception; import org.springframework.security.web.authentication.simpleurlauthenticationfailurehandler; import org.springframework.stereotype.component; import com.fasterxml.jackson.databind.objectmapper; import lombok.extern.slf4j.slf4j; @slf4j @component("myauthenctiationfailurehandler") public class myauthenctiationfailurehandler extends simpleurlauthenticationfailurehandler { @autowired private objectmapper objectmapper; @override public void onauthenticationfailure(httpservletrequest request, httpservletresponse response, authenticationexception exception) throws ioexception, servletexception { log.info("登录失败"); response.setstatus(httpstatus.internal_server_error.value()); response.setcontenttype("application/json;charset=utf-8"); response.getwriter().write(objectmapper.writevalueasstring(exception.getmessage())); } }
将两个自定义的处理类配置到自定义配置文件中:
import org.springframework.beans.factory.annotation.autowired; import org.springframework.context.annotation.configuration; 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.bcrypt.bcryptpasswordencoder; import org.woodwhale.king.handler.myauthenctiationfailurehandler; import org.woodwhale.king.handler.myauthenctiationsuccesshandler; @configuration @enablewebsecurity public class websecurityconfig extends websecurityconfigureradapter { @autowired private myauthenctiationfailurehandler myauthenctiationfailurehandler; @autowired private myauthenctiationsuccesshandler myauthenctiationsuccesshandler; @override protected void configure(httpsecurity http) throws exception { http.formlogin() // 定义当需要用户登录时候,转到的登录页面。 .loginpage("/login") // 设置登录页面 .loginprocessingurl("/user/login") // 自定义的登录接口 .successhandler(myauthenctiationsuccesshandler) .failurehandler(myauthenctiationfailurehandler) //.defaultsuccessurl("/home").permitall() // 登录成功之后,默认跳转的页面 .and().authorizerequests() // 定义哪些url需要被保护、哪些不需要被保护 .antmatchers("/", "/index").permitall() // 设置所有人都可以访问登录页面 .anyrequest().authenticated() // 任何请求,登录后可以访问 .and().csrf().disable(); // 关闭csrf防护 } @override protected void configure(authenticationmanagerbuilder auth) throws exception { auth.inmemoryauthentication() .passwordencoder(new bcryptpasswordencoder()).withuser("admin") .password(new bcryptpasswordencoder().encode("admin")) .roles("admin"); } }
注意:defaultsuccessurl
不需要再配置了,实测如果配置了,成功登录的 handler 就不起作用了。
小结
可以看出,通过自定义的登录成功或者失败类,进行登录响应控制,可以设计一个配置,以灵活适配响应返回的是页面还是 json 数据。
结合thymeleaf
在前端使用了thymeleaf
进行渲染,特使是结合spring security
在前端获取用户信息
依赖添加:
<dependency> <groupid>org.thymeleaf.extras</groupid> <artifactid>thymeleaf-extras-springsecurity5</artifactid> </dependency>
注意:
因为本项目使用了spring boot 自动管理版本号,所以引入的一定是完全匹配的,如果是旧的 spring security 版本需要手动引入对应的版本。
引用官方版本引用说明:
thymeleaf-extras-springsecurity3 for integration with spring security 3.x thymeleaf-extras-springsecurity4 for integration with spring security 4.x thymeleaf-extras-springsecurity5 for integration with spring security 5.x
具体语法可查看:
https://github.com/thymeleaf/thymeleaf-extras-springsecurity
常用的语法标签
这里为了表述方便,引用了上小节中的"自定义处理登录成功/失败"的成功响应json数据:
{ "authorities": [ { "authority": "role_admin" } ], "details": { "remoteaddress": "127.0.0.1", "sessionid": "8bfa4f61a7cea774c00f616aae8c307c" }, "authenticated": true, "principal": { "password": null, "username": "admin", "authorities": [ { "authority": "role_admin" } ], "accountnonexpired": true, "accountnonlocked": true, "credentialsnonexpired": true, "enabled": true }, "credentials": null, "name": "admin" }
sec:authorize="isauthenticated()
:判断是否有认证通过
sec:authorize="hasrole('role_admin')"
判断是否有role_admin
权限
注意:上述的hasrole()
标签使用能成功的前提是:自定义用户的权限字符集必须是以role_
为前缀的,否则解析不到,即自定义的userdetailsservice
实现类的返回用户的权限数组列表的权限字段必须是role_***
,同时在 html 页面中注意引入对应的xmlns
,本例这里引用了:
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
sec:authentication="principal.authorities"
:得到该用户的所有权限列表
sec:authentication="principal.username"
:得到该用户的用户名
当然也可以获取更多的信息,只要userdetailsservice
实现类中返回的用户中携带有的信息均可以获取。
常见异常类
authenticationexception 常用的的子类:(会被底层换掉,不推荐使用) usernamenotfoundexception 用户找不到 badcredentialsexception 坏的凭据 accountstatusexception 用户状态异常它包含如下子类:(推荐使用) accountexpiredexception 账户过期 lockedexception 账户锁定 disabledexception 账户不可用 credentialsexpiredexception 证书过期
参考资料:
https://mp.weixin.qq.com/s/nkhwu6qkku0q0dia0hg13q
https://mp.weixin.qq.com/s/smi1__rw_s75ydaidmtwkw
参考项目源码:
上一篇: 搞笑生活类型的QQ表情动态图片
下一篇: 这样着装才叫夫妻,才是情侣,生死不分离。