文章目录
一、Spring Security简介
- Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。
- Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求
- Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。
- 百度百科——Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
二、Spring Security的新特性
Spring Security 5.1 中文文档下载;提取码:801i
-
1、OAuth2
OAuth2 Resource Server
添加了对 OAuth2 资源服务器的基本支持。见 oauth2resourceserver -
2、Authorization Code Flow
用户现在可以使用OAuth 2.0 Authorization Code grant 获取访问令牌(access token)。请查看 authcodegrant 示例。
-
3、支持 WebClient 和 OAuth2
现在内置了对 OAuth2 和 WebClient 的支持,该支持将允许:
-
将访问令牌添加到请求中
-
访问令牌过期时自动刷新
-
解析要使用的访问令牌
-
-
4、WebFlux OAuth2 Log In Supports OIDC
WebFlux 应用程序现在可以使用 OAuth2 和 OIDC 进行身份验证。有关详细示例,请查看 oauth2login-webflux。
-
5、配置改进
改进默认登录页面,更现代化
默认登录页面实用了 HTML5,变得更现代化,看起来更具视觉吸引力。 -
6、默认注销页面
由于添加了 CSRF 注销保护,因此默认应用程序无法注销。现在,如果正在使用默认登录页面(例如没有配置登录页面),那么还会有一个默认的注销页面,它显示了一个注销表单。
-
7、简化 RequestCache 配置
用户现在可以配置默认的 RequestCache,用于将其暴露给@Bean。
更多请参考官方文档
三、SpringBoot 2.X和 Spring Security5.X的整合(hello world版本)
- 首先得安装配置好Maven
- 还有对springboot的基本了解,可参考SpringBoot基础技术笔记视频资料
- 还有联网。
1、使用idea的Spring Initializr来初始化项目
导入Web模块的starter和Security
2、查看SpringBoot 与Spring Security 的版本
可以打开pom.xml。右键查看依赖的版本控制
3、直接运行主程序类。
直接访问 http://localhost:8080,默认会追加上/login
4、分析
目录结构只是Maven生成的。我们什么都没做。为什么?
有兴趣的可以去官方文档查找答案。
在这里我就直接说了:
1、我们打开主配置类;
2、摁两下shift弹出搜索框,然后搜索WebSecurityConfigurerAdapter
,我们来打开这个类;(也可以直接在左边的项目工程中,找到External Libraries
我们来找到这个org.springframework.security.config.annotation.web.configuration
)也是找到这个类。
protected void configure(HttpSecurity http) throws Exception {
this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests()
.anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
}
3、在WebSecurityConfigurerAdapter
这个抽象类中,我们搜索formlogin
来查看这方法。
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
return (FormLoginConfigurer)this.getOrApply(new FormLoginConfigurer());
}
//可以看到它返回的是FormLoginConfigurer<HttpSecurity>这个类型的数据,我们继续点进去(ctrl+鼠标左键),查看FormLoginConfigurer这个类
4、查看FormLoginConfigurer
类翻到最下面。
private void initDefaultLoginFilter(H http) {
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = (DefaultLoginPageGeneratingFilter)http.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter != null && !this.isCustomLoginPage()) {
loginPageGeneratingFilter.setFormLoginEnabled(true);
loginPageGeneratingFilter.setUsernameParameter(this.getUsernameParameter());
loginPageGeneratingFilter.setPasswordParameter(this.getPasswordParameter());
loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage());
loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl());
loginPageGeneratingFilter.setAuthenticationUrl(this.getLoginProcessingUrl());
}
}
//可以看到这个方法做了我们默认的登录页是否存在的判断,大家可以自己查看isCustomLoginPage()
//这个方法返回的是布尔值,方法的含义的我个人理解是:是默认登录页吗?
//如果不是的话,loginPageGeneratingFilter不是空的话,就给loginPageGeneratingFilter做一些设置。
5、这个过滤器做了什么事情?
我们点DefaultLoginPageGeneratingFilter
查看这个类。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
boolean loginError = this.isErrorPage(request);
boolean logoutSuccess = this.isLogoutSuccess(request);
if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
chain.doFilter(request, response);
} else {
String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
}
}
//可以看到,登录页面是由generateLoginPageHtml()方法构造的
6、generateLoginPageHtml()
方法就在dofilte方法下面,也可以直接点过去
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
String errorMsg = "Invalid credentials";
if (loginError) {
HttpSession session = request.getSession(false);
if (session != null) {
AuthenticationException ex = (AuthenticationException)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";
}
}
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n <meta name=\"description\" content=\"\">\n <meta name=\"author\" content=\"\">\n <title>Please sign in</title>\n <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n </head>\n <body>\n <div class=\"container\">\n");
String contextPath = request.getContextPath();
if (this.formLoginEnabled) {
sb.append(" <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n <h2 class=\"form-signin-heading\">Please sign in</h2>\n" + createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n <label for=\"username\" class=\"sr-only\">Username</label>\n <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n </p>\n <p>\n <label for=\"password\" class=\"sr-only\">Password</label>\n <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n </p>\n" + this.createRememberMe(this.rememberMeParameter) + this.renderHiddenInputs(request) + " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n </form>\n");
}
if (this.openIdEnabled) {
sb.append(" <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n" + createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n <label for=\"username\" class=\"sr-only\">Identity</label>\n <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n </p>\n" + this.createRememberMe(this.openIDrememberMeParameter) + this.renderHiddenInputs(request) + " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n </form>\n");
}
if (this.oauth2LoginEnabled) {
sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
sb.append(createError(loginError, errorMsg));
sb.append(createLogoutSuccess(logoutSuccess));
sb.append("<table class=\"table table-striped\">\n");
Iterator var7 = this.oauth2AuthenticationUrlToClientName.entrySet().iterator();
while(var7.hasNext()) {
Entry<String, String> clientAuthenticationUrlToClientName = (Entry)var7.next();
sb.append(" <tr><td>");
String url = (String)clientAuthenticationUrlToClientName.getKey();
sb.append("<a href=\"").append(contextPath).append(url).append("\">");
String clientName = HtmlUtils.htmlEscape((String)clientAuthenticationUrlToClientName.getValue());
sb.append(clientName);
sb.append("</a>");
sb.append("</td></tr>\n");
}
sb.append("</table>\n");
}
sb.append("</div>\n");
sb.append("</body></html>");
return sb.toString();
}
我们的页面呢就是这样一步步构造出来的。
5、debug=true
接下来我们在资源文件夹下面的application.properties
文件中加一行代码debug=true
然后我们需要编写一个简单的controler
package com.sjt.springbootsecurity.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author easychill
* @version V1.0
* @Package com.sjt.springbootsecurity.controller
* @date 2019/7/31 13:13
*/
@RestController
public class TestController {
@GetMapping("/")
public String hello() {
return "hello security!";
}
}
然后重新把项目跑一遍,不要着急,我们可以看到控制台打印了一些自动匹配的配置类,和没有匹配的。
翻到很上面的一个位置应该可以看到如下内容:
然后复制密码,刷新我们的页面 http://localhost:8080,还是来到了登录页面。账号是user
,密码是你复制的那个。登录…然后就可以看到这个东西
奇怪吗?账号密码是哪里来的?
6、账号密码从哪来?
按照之前的套路,我们直接,shift+shift,搜索UserDetailsServiceAutoConfiguration
,在这个类中,可以看到getOrDeducePassword()
这个方法,这样密码为什么会出现在打印台就一目了然了。在这个方法的形参中我们看到了User类!我们根本没有定义过,说明是封装好的。点进去!
这样的话,就很清楚了。