Spring Security认证之基本认证
创始人
2024-01-22 06:23:41

本文内容来自王松老师的《深入浅出Spring Security》,自己在学习的时候为了加深理解顺手抄录的,有时候还会写一些自己的想法。

快速入门

        在Spring Boot项目中使用Spring Security非常方便,创建一个新的Spring Boot项目我们只要引入Web和Spring Security依赖即可,具体的pom依赖如下:

         org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-security

        然后我们在项目中提供一个简单的测试/hello接口,代码如下:

/*** @author tlh* @date 2022/11/15 21:25*/
@RestController
public class HelloController {@GetMapping("/hello")public String hello() {return "hello spring security";}
}

        接下来我们启动项目,/hello接口就会被Spring Seciryt保护起来。当用户访问/hello接口是就会跳转到登录页面(如下图),用户登录成功之后才能正常访问/hello接口。

        默认的登录用户名是user,登录密码则是一个随机生成的UUID字符串,在项目的启动日志中可以看到(也就意味着没项目启动密码都会发生变化,这里只是我们体验Spring Security使用的,后面我们会把用户名和密码存到数据库)

        输入默认的用户名和密码就能正常的访问/hello接口了。这里简单的体验下了Spring Security的强大之处,只需要映入一个简单的依赖所有的接口就会被自动保护起来。

流程分析

        通过几个简单的流程图来看一下上面案例中的请求流程:

  1.  客户端(浏览器)发起请求去访问/hello接口,这个接口默认是需要认证之后才能访问的。
  2. 这个请求户走一遍Spring Security中的过滤器链,在最后的FilterSecurityInterceptor过滤器中拦截下来。因为系统中发现用户没有被认证,请求拦截下来之后会抛出AccessDeniedException异常。
  3. 抛出的AccessDeniedException异常在ExceptionTranslationFilter过滤器中被捕获,ExceptionTranslationFilter过滤器通过调用LoginUrlAuthenticationEntryPoint的commence方法给客户端返回302,要求客户端重定向到/login页面。
  4. 客户端发送/login请求
  5. /login请求被DefaultLoginPageGeneratinFilter过滤器拦截下来,并在改过滤器中返回登录页面。所以用户访问/hello接口会先看到登录页面。

        整个过程中,相当于客户端发送了两次请求,第一个请求是/hello,服务端收到之后返回302,请求客户端重定向到/longin,于是客户端又发送了/login请求。

        此时去理解这个流程可能还有点困哪,等看完接下的文章之后再回头来看这个流程图应该就会比较清晰了。

原理分析

        虽然开发者只是引入了一个Spring Security相关的依赖,代码并不多,但是Spring Security背后为我们默默的做了很多事情:

  • 开启Spring Security自动化配置。开启后,Spring Security会自动创建一个名为springSecurityFilterChain的过滤器并注入到Spring容器中,这个过滤器将负责所有的安全管理,包括用户的认证、授权、重定向到登录页面等(springSecurityFilterChain实际上代理了Spring Security中的过滤器链)
  • 创建一个UserDetailsService实例,UserDetailsService负责提供用户数据,默认用户数据是基于内存的用户,用户名为user,密码则是随机生成的UUID字符串
  • 给用户生成一个默认的登录页面

默认用户生成

        Spring Security中定义了UserDetails接口来规范开发者自定义用户对象,这样方便一些旧系统、用户表已经固定的系统集成Spring Security认证体系中。

        UserDetails接口定义如下:

public interface UserDetails extends Serializable {//返回当前用户所具备的权限Collection getAuthorities();//返回当前用户的密码String getPassword();//返回当前用户名String getUsername();//返回当前用户是否已经过期boolean isAccountNonExpired();//返回当前用户是否被锁定boolean isAccountNonLocked();//返回当前用户的凭证是否未过期boolean isCredentialsNonExpired();//返回当前账户是否可用boolean isEnabled();
}

        负责提供用户数据的接口是UserDetailsService。UserDetailsService接口中只有一个查询用户的方法,代码如下:

public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

        loadUserByUsername只有一个username参数,这是用户在认证时传入的用户名,最常见的就是表单中输入的用户名。开发者在这里拿到用户名之后,再去数据库中查询用户,最终返回一个UserDetails实例。在实际开发中,一般开发者需要自定义UserDetailsService的实现。如果开发者没有自定义UserDetailsService的实现,Spring Securit也为UserDetailsService提供了默认实现:

  • UserDetailsManager:这个是一个扩展接口,新增了添加用户、跟新用户、删除用户、修改密码、判断用户是否存在等等方法。
  • JdbcDaoImpl:实现了通过spring-jdbc冲数据库中查询用户的方法
  • JdbcUserDetailsManager:继承自JdbcDaoImpl同时又实现了UserDetailsManager接口。这里有一定的局限性,因为操作数据的sql都是写好的不够灵活。因此,在实际开发中JdbcUserDetailsManager使用的并不多。
  • InMemoryUserDetailsManager:实现了UserDetailsManager中关于用户的增、删、改、查方法,不过都是基于内存操作,数据并没有持久化。

        当我们使用Spring Security时,如果仅仅引入一个Spring Security的依赖,则默认使用的InMemoryUserDetailsManager。伙伴们都知道,Spring Boot之所以能做到零配置使用Spring Security,就是因为它提供了众多的自动化配置类。其中UserDetailsService的自动化配置类就是UserDetailsAotuConfiguration。如下:

@AutoConfiguration
@ConditionalOnClass({AuthenticationManager.class})
@ConditionalOnBean({ObjectPostProcessor.class})
@ConditionalOnMissingBean(value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class},type = {"org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository"}
)
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);public UserDetailsServiceAutoConfiguration() {}@Bean@Lazypublic InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider passwordEncoder) {User user = properties.getUser();List roles = user.getRoles();return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});}private String getOrDeducePassword(User user, PasswordEncoder encoder) {String password = user.getPassword();if (user.isPasswordGenerated()) {logger.warn(String.format("%n%nUsing generated security password: %s%n%nThis generated password is for development use only. Your security configuration must be updated before running your application in production.%n", user.getPassword()));}return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;}
}

        从上面的代码可以看到,有两个比较重要的条件使系统自动提供一个InMemoryUserDetailsManager的实例:

  • 当前的classpath下没有AuthenticationManager类
  • 当前项目中,系统没有提供AuthenticationManager, AuthenticationProvider, UserDetailsService, ClientRegistrationRepository实例

        默认情况下,上面的条件都满足。此时Spring Security会提供一个InMemoryUserDetailsManager实例。从InMemoryUserDetailsManager方法中看到,用户信息来自于SecurityProperties的getUser方法。我们就能看到默认用户的名字为:user,密码则是UUID的随机字符串。

@ConfigurationProperties(prefix = "spring.security"
)
public class SecurityProperties {public static final int BASIC_AUTH_ORDER = 2147483642;public static final int IGNORED_ORDER = -2147483648;public static final int DEFAULT_FILTER_ORDER = -100;private final SecurityProperties.Filter filter = new SecurityProperties.Filter();private final SecurityProperties.User user = new SecurityProperties.User();public SecurityProperties() {}public SecurityProperties.User getUser() {return this.user;}public SecurityProperties.Filter getFilter() {return this.filter;}public static class User {private String name = "user";private String password = UUID.randomUUID().toString();private List roles = new ArrayList();//省略get/set方法}}

        我们看到SecurityProperties 类在加载的时候还可从配置文件中读取信息(@ConfigurationProperties注解起的作用),前缀为spring.security。这就意味着我们可以在配置文件中(application.properties)配置默认用户名和密码:

spring.security.user.name=tlh
spring.security.user.password=123456
spring.security.user.roles=admin,users

        配置完成之后,重启项目此时登录名就是tlh,登录密码就是123456,登录成功之后用户具备admin和users两个角色。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
ps局部放大 放大某个部位的图... 软件类学习建议:边看边练习,可以先收藏后学习。一、使用Ctrl快捷键放大:Ctrl + +缩小:Ct...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
ps局部放大 放大某个部位的图... 软件类学习建议:边看边练习,可以先收藏后学习。一、使用Ctrl快捷键放大:Ctrl + +缩小:Ct...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...