对于安全管理框架而言,认证功能可以说是一切的起点,所以我们要研究Spring Security, 就要从最基本的认证开始。在Spring Security中,对认证功能做了大量的封装,以至于开发者只需要稍微配置一下就能使用认证功能,然而要深刻理解其源码却并非易事。本文从最基本的用法开始讲解,最终再扩展到对源码的理解。
本章涉及的主要知识点有:
在Spring Boot项目中使用Spring Security非常方便,创建一个新的Spring Boot项目,我 们只需要引入Web和Spring Security依赖即可,具体代码如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
然后我们在项目中提供一个用于测试的/hello接口,代码如下
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello spring security";
}
}
接下来启动项目,/hello接口就已经被自动保护起来了。当用户访问/hello接口时,会自动跳转到登录页面,如图所示,用户登录成功后,才能访问到/hello接口。

默认的登录用户名是user,登录密码则是一个随机生成的UUID字符串,在项目启动日志中可以看到登录密码(这也意味着项目每次启动时,密码都会发生变化):
Using generated security password: 8ef9c800-17cf-47a3-9984-8ff936db6dd8
输入默认的用户名和密码,就可以成功登录了,这就是Spring Security的强大之处,只需要引入一个依赖,所有的接口就会被自动保护起来。
通过一个简单的流程图来看一下上面案例中的请求流程,如下图所示

流程图比较清晰地说明了整个请求过程:
在整个过程中,相当于客户端一共发送了两个请求,第一个请求是/hello,服务端收到之 后,返回302,要求客户端重定向到/login,于是客户端又发送了/login请求。现在去理解上面这一个流程图可能还有些困难,等学完后面的内容之后,再回过头来看这个流程图,应该就会比较清晰了。
幵启Spring Security自动化配置,开启后,会自动创建一个名为springSecurityFilterChain 的过滤器,并注入到Spring容器中,这个过滤器将负责所有的安全管理,包括用户的认证、授权、重定向到登录页面等(springSecmityFilterChain实际上代理了 Spring Security中的过滤器链)。
这里涉及的细节还是非常多的,登录的细节会在后面详细介绍,这里主要分析一下默认用户的生成以及默认登录页面的生成
Spring Security中定义了 UserDetails接口来规范开发者自定义的用户对象,这样方便一些旧系统、用户表己经固定的系统集成到Spring Security认证体系中。
UserDetails接口定义如下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
该接口中一共定义了 7个方法:
这是用户对象的定义,而负责提供用户数据源的接口是UserDetailsService , UserDetailsService中只有一个查询用户的方法,代码如下:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
loadUserByUsername有一个参数是username,这是用户在认证时传入的用户名,最常见的就是用户在登录表单中输入的用户名(实际开发时还可能存在其他情况,例如使用CAS单点登录时,username并非表单输入的用户名,而是CAS Server认证成功后回调的用户名参数), 开发者在这里拿到用户名之后,再去数据库中査询用户,最终返回一个UserDetails实例。
在实际项目中,一般需要开发者自定义UserDetailsService的实现。如果开发者没有自定义 UserDetailsService 的实现,Spring Security 也为 UserDetailsService 提供了默认实现,如下图

当我们使用Spring Security时,如果仅仅只是引入一个Spring Security依赖,则默认使用的用户就是由 InMemoryUserDetailsManager 提供的。
大家知道,Spring Boot之所以能够做到零配置使用Spring Security,就是因为它提供了众多的自动化配置类,其中,针对UserDetailsService的自动化配置类是UserDetailsServiceAuto Configurationr这个类的源码并不长,我们一起来看一下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class },
type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector" })
public class UserDetailsServiceAutoConfiguration {
private static final String NOOP_PASSWORD_PREFIX = "{noop}";
private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);
@Bean
@ConditionalOnMissingBean(
type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}
private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
return password;
}
return NOOP_PASSWORD_PREFIX + password;
}
}
上述代码中可以看到,有两个比较重要的条件促使系统自动提供一个InMemoryUserDetailsManager 的实例:
(1)当前 classpath 下存在 AuthenticationManager 类。
(2 )当前项目中,系统没有提供 AuthenticationManager、AutlienticationProvider、UserDetailsService 以及 ClientRegistrationRepository 实例。
默认情况下,上面的条件都会满足,此时Spring Security会提供一个InMemoryUserDetailsManager实例。从InMemoryUserDetailsManager方法中可以看到,用户数据源自 SecurityProperties#getUser 方法:
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
public static final int BASIC_AUTH_ORDER = Ordered.LOWEST_PRECEDENCE - 5;
public static final int IGNORED_ORDER = Ordered.HIGHEST_PRECEDENCE;
public static final int DEFAULT_FILTER_ORDER = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100;
private final Filter filter = new Filter();
private User user = new User();
public User getUser() {
return this.user;
}
public Filter getFilter() {
return this.filter;
}
public static class Filter {
private int order = DEFAULT_FILTER_ORDER;
private Set<DispatcherType> dispatcherTypes = new HashSet<>(
Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Set<DispatcherType> getDispatcherTypes() {
return this.dispatcherTypes;
}
public void setDispatcherTypes(Set<DispatcherType> dispatcherTypes) {
this.dispatcherTypes = dispatcherTypes;
}
}
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList<>();
private boolean passwordGenerated = true;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
if (!StringUtils.hasLength(password)) {
return;
}
this.passwordGenerated = false;
this.password = password;
}
public List<String> getRoles() {
return this.roles;
}
public void setRoles(List<String> roles) {
this.roles = new ArrayList<>(roles);
}
public boolean isPasswordGenerated() {
return this.passwordGenerated;
}
}
}
从SecurityProperties.User类中,我们就可以看到默认的用户名是user,默认的密码是一个 UUID字符串。
再回到 InMemoryUserDetailsManager方法中,构造 InMemoryUserDetailsManager实例时需要一个 User对象。这里的 User对象不是SecurityProperties.User ,而是 org.springframework.security.core.userdetails.User, 这是 Spring Security 提供的一个实现了UserDetails接口的用户类,该类提供了相应的静态方法,用来构造一个默认的Uset实例。同时,默认的用户密码还在getOrDeducePassword方法中进行了二次处理,由于默认的encoder 为null,所以密码的二次处理只是给密码加了一个前缀{noop},表示密码是明文存储的(关于 {noop}将在后续密码加密中做详细介绍)。
经过以上的源码梳理,相信大家已经明白了 Spring Security 默认的用户名/密码是来自哪里了!另外,当看了 Security Properties的源码后,只要对Spring Boot中properties属性的加载机制有一点了解,就会明白,只要我们在项目的application.properties配置文件中添加如下配置, 就能定制SecurityProperties.User类中各属性的值:
spring.security.user.name=javaboy
spring.security.user.password=123
spring.security.user.roles=admin, user
配置完成后,重启项目,此时登录的用户名就是javaboy,登录密码就是123 ,登录成功后用户具备admin和user两个角色。
在上面的案例中,一共存在两个默认页面,一个就是默认的登录页面,另外一个则是注销登录页面。当用户登录成功之后,在浏览器中输入http://localhost:8080/logout就可以看到注销登录页面,如图所示。

那么这两个页面是从哪里来的呢?这里剖析一下,
在前面我们介绍了 Spring Security中常见的过滤器,在这些常见的过滤器中就包含两个和页面相关的过滤器:DefaultLoginPageGeneratingFilter 和 DefaultLogoutPageGeneratingFilter
通过过滤器的名字就可以分辨出DefaultLoginPageGeneratingFilter过滤器用来生成默认的登录页面,DefaultLogoutPageGeneratingFilter过滤器则用来生成默认的注销页面。
先来看 DefaultLoginPageGeneratingFilter 作为 Spring Security 过滤器链中的一员,在第一次请求/hello接口的时候,就会经过DefaultLoginPageGeneratingFilter过滤器,但是由于/hello 接口和登录无关,因此DefaultLoginPageGeneratingFilter过滤器并未干涉/hello接口,等到第二次重定向到/login页面的时候,这个时候就和DefaultLoginPageGeneratingFilter有关系了,此时请求就会在DefaultLoginPageGeneratingFilter中进行处理,生成登录页面返回给客户端。
我们来看一下DefaultLoginPageGeneratingFilter的源码,源码比较长,这里仅列出核心部分:
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
String loginPageHtml = generateLoginPageHtml(request, loginError,
logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.length());
response.getWriter().write(loginPageHtml);
return;
}
chain.doFilter(request, response);
}
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
boolean logoutSuccess) {
String errorMsg = "none";
if (loginError) {
HttpSession session = request.getSession(false);
if (session != null) {
AuthenticationException ex = (AuthenticationException) session
.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
errorMsg = ex != null ? ex.getMessage() : "none";
}
}
StringBuilder sb = new StringBuilder();
sb.append("<html><head><title>Login Page</title></head>");
if (formLoginEnabled) {
sb.append("<body onload='document.f.").append(usernameParameter)
.append(".focus();'>\n");
}
if (loginError) {
sb.append("<p style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
sb.append(errorMsg);
sb.append("</p>");
}
if (logoutSuccess) {
sb.append("<p style='color:green;'>You have been logged out</p>");
}
if (formLoginEnabled) {
sb.append("<h3>Login with Username and Password</h3>");
sb.append("<form name='f' action='").append(request.getContextPath())
.append(authenticationUrl).append("' method='POST'>\n");
sb.append("<table>\n");
sb.append(" <tr><td>User:</td><td><input type='text' name='");
sb.append(usernameParameter).append("' value='").append("'></td></tr>\n");
sb.append(" <tr><td>Password:</td><td><input type='password' name='")
.append(passwordParameter).append("'/></td></tr>\n");
if (rememberMeParameter != null) {
sb.append(" <tr><td><input type='checkbox' name='")
.append(rememberMeParameter)
.append("'/></td><td>Remember me on this computer.</td></tr>\n");
}
sb.append(" <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
renderHiddenInputs(sb, request);
sb.append("</table>\n");
sb.append("</form>");
}
if (openIdEnabled) {
sb.append("<h3>Login with OpenID Identity</h3>");
sb.append("<form name='oidf' action='").append(request.getContextPath())
.append(openIDauthenticationUrl).append("' method='POST'>\n");
sb.append("<table>\n");
sb.append(" <tr><td>Identity:</td><td><input type='text' size='30' name='");
sb.append(openIDusernameParameter).append("'/></td></tr>\n");
if (openIDrememberMeParameter != null) {
sb.append(" <tr><td><input type='checkbox' name='")
.append(openIDrememberMeParameter)
.append("'></td><td>Remember me on this computer.</td></tr>\n");
}
sb.append(" <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
sb.append("</table>\n");
renderHiddenInputs(sb, request);
sb.append("</form>");
}
if (oauth2LoginEnabled) {
sb.append("<h3>Login with OAuth 2.0</h3>");
sb.append("<table>\n");
for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
sb.append(" <tr><td>");
sb.append("<a href=\"").append(request.getContextPath()).append(clientAuthenticationUrlToClientName.getKey()).append("\">");
sb.append(clientAuthenticationUrlToClientName.getValue());
sb.append("</a>");
sb.append("</td></tr>\n");
}
sb.append("</table>\n");
}
sb.append("</body></html>");
return sb.toString();
}
}
DefaultLoginPageGeneratingFliter的源码执行流程还是非常清晰的,我们梳理一下:
这就是DefaultLoginPageGeneratingFilter的工作过程。这里重点搞明白为什么/hello请求没有被拦截,而/login请求却被拦截了,其他都很好懂。
理解了 DefaultLoginPageGeneratingFilter,再来看 DefaultLogoutPageGeneratingFilter 就更容易了,DefaultLogoutPageGeneratingFilter 部分核心源码如下 :
public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter {
private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET");
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (this.matcher.matches(request)) {
this.renderLogout(request, response);
} else {
filterChain.doFilter(request, response);
}
}
private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
String page = "<!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>Confirm Log Out?</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 <form class=\"form-signin\" method=\"post\" action=\"" + request.getContextPath() + "/logout\">\n <h2 class=\"form-signin-heading\">Are you sure you want to log out?</h2>\n" + this.renderHiddenInputs(request) + " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n </form>\n </div>\n </body>\n</html>";
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(page);
}
}
从上述源码中可以看出,请求到来之后,会先判断是否是注销请求/logout,如果是/logout 请求,则渲染一个注销请求的页面返回给客户端,渲染过程和前面登录页而的渲染过程类似, 也是字符串拼接(这里省略了字符串拼接,读者可以参考DefaultLogoutPageGeneratingFilter 的源码);否则请求继续往下走,执行下一个过滤器。
通过前面的分析,相信大家对这个简单的案例己经有所了解,看似只是加了一个依赖, 但实际上Spring Security和Spring Boot在背后都默默做了很多事情,当然还有很多没有介绍到的,将在后面和大家一起继续深究。
?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------
项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU
Rails相对较新。我正在尝试调用一个API,它应该向我返回一个唯一的URL。我的应用程序中捆绑了HTTParty。我已经创建了一个UniqueNumberController,并且我已经阅读了几个HTTParty指南,直到我想要什么,但也许我只是有点迷路,真的不知道该怎么做。基本上,我需要做的就是调用API,获取它返回的URL,然后将该URL插入到用户的数据库中。谁能给我指出正确的方向或与我分享一些代码? 最佳答案 假设API为JSON格式并返回如下数据:{"url":"http://example.com/unique-url"
简单代码require'net/http'url=URI.parse('getjson/otherdatahere[link]')req=Net::HTTP::Get.new(url.to_s)res=Net::HTTP.start(url.host,url.port){|http|http.request(req)}putsres.body只是想知道如何在phpcURL中放置身份验证token,我是这样做的 curl_setopt($ch,CURLOPT_HTTPHEADER,array('Authorization:Bearerxxx'));//Bearertokenfora
我正在尝试复制此GETcurl请求:curl-D--XGET-H"Authorization:BasicdGVzdEB0YXByZXNlYXJjaC5jb206NGMzMTg2Mjg4YWUyM2ZkOTY2MWNiNWRmY2NlMTkzMGU="-H"Content-Type:application/json"http://staging.example.com/api/v1/campaigns在Ruby中,通过电子邮件+apikey生成身份验证:auth="Basic"+Base64::encode64("test@example.com:4c3186288ae23fd9661c
谁能提供一个使用HTTParty和digestauth的例子?我在网上找不到例子,希望有人能提供一些帮助。谢谢。 最佳答案 您可以在定义类时使用digest_auth方法设置用户名和密码classFooincludeHTTPartydigest_auth'username','password'end 关于ruby-HTTParty摘要认证,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questi
文章目录一、项目场景二、基本模块原理与调试方法分析——信源部分:三、信号处理部分和显示部分:四、基本的通信链路搭建:四、特殊模块:interpretedMATLABfunction:五、总结和坑点提醒一、项目场景 最近一个任务是使用simulink搭建一个MIMO串扰消除的链路,并用实际收到的数据进行测试,在搭建的过程中也遇到了不少的问题(当然这比vivado里面的debug好不知道多少倍)。准备趁着这个机会,先以一个很基本的通信链路对simulink基础和相关的debug方法进行总结。 在本篇中,主要记录simulink的基本原理和基本的SISO通信传输链路(QPSK方式),计划在下篇记
我找不到任何使用Rack::Session::Cookie的简单示例,并且希望能够将信息存储在cookie中,并在以后的请求中访问它并让它过期.这些是我能找到的唯一示例:HowdoIset/getsessionvarsinaRackapp?http://rack.rubyforge.org/doc/classes/Rack/Session/Cookie.html这是我得到的:useRack::Session::Cookie,:key=>'rack.session',:domain=>'foo.com',:path=>'/',:expire_after=>2592000,:secret=
我正在尝试使用GnipPowerTrackAPI,这需要我使用基本身份验证连接到JSON的HTTPS流。我觉得这应该是相当微不足道的,所以我希望一些比我聪明的rubyist可以指出我明显的错误。这是我的ruby1.9.3代码的相关部分:require'eventmachine'require'em-http'require'json'usage="#{$0}"abortusageunlessuser=ARGV.shiftabortusageunlesspassword=ARGV.shiftGNIP_STREAMING_URL='https://stream.gnip.com:4
本人是音乐爱好者,从小就特别喜欢那个随着音乐跳动的方框效果,就是这个:arduino上一大把对,我忍你很久了,我就想用mpy做,全网没有,行我自己研究。果然兴趣是最好的老师,我之前有篇博客专门讲音频,有兴趣的可以回顾一下。提到可视化频谱,必然绕不开fft,大学学过这玩意,当时一心玩,老师讲的一个字都么听进去,网上教程简略扫了一下,大该就是把时域转频域的工具,我大mpy居然没有fft函数,奶奶的,先放着。音频信息如何收集?第一种傻瓜式的ADC,模拟转数字,原始粗暴,第二种,I2S库,我之前博客有讲过,数据是PCM编码。然后又去学PCM编码,一学豁然开朗,舒服,以代码为例:audio_in=I2S