Spring Security UserDetails实现原理详解
1.前言
今天开始我们来一步步窥探它是如何工作的。我们又该如何驾驭它。本篇将通过SpringBoot2.x来讲解SpringSecurity中的用户主体UserDetails。以及从中找点乐子。
2.SpringBoot集成SpringSecurity
这个简直老生常谈了。不过为了照顾大多数还是说一下。集成SpringSecurity只需要引入其对应的Starter组件。SpringSecurity不仅仅能保护ServletWeb应用,也可以保护ReactiveWeb应用,本文我们讲前者。我们只需要在SpringSecurity项目引入以下依赖即可:
org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.security spring-security-test test
3.UserDetailsServiceAutoConfiguration
启动项目,访问Actuator端点http://localhost:8080/actuator会跳转到一个登录页面http://localhost:8080/login如下:
要求你输入用户名Username(默认值为user)和密码Password。密码在springboot控制台会打印出类似Usinggeneratedsecuritypassword:e1f163be-ad18-4be1-977c-88a6bcee0d37的字样,后面的长串就是密码,当然这不是生产可用的。如果你足够细心会从控制台打印日志发现该随机密码是由UserDetailsServiceAutoConfiguration配置类生成的,我们就从它开始顺藤摸瓜来一探究竟。
3.1UserDetailsService
UserDetailsService接口。该接口只提供了一个方法:
UserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException;
该方法很容易理解:通过用户名来加载用户。这个方法主要用于从系统数据中查询并加载具体的用户到SpringSecurity中。
3.2UserDetails
从上面UserDetailsService可以知道最终交给SpringSecurity的是UserDetails。该接口是提供用户信息的核心接口。该接口实现仅仅存储用户的信息。后续会将该接口提供的用户信息封装到认证对象Authentication中去。UserDetails默认提供了:
- 用户的权限集,默认需要添加ROLE_前缀
- 用户的加密后的密码,不加密会使用{noop}前缀
- 应用内唯一的用户名
- 账户是否过期
- 账户是否锁定
- 凭证是否过期
- 用户是否可用
如果以上的信息满足不了你使用,你可以自行实现扩展以存储更多的用户信息。比如用户的邮箱、手机号等等。通常我们使用其实现类:
org.springframework.security.core.userdetails.User
该类内置一个建造器UserBuilder会很方便地帮助我们构建UserDetails对象,后面我们会用到它。
3.3UserDetailsServiceAutoConfiguration
UserDetailsServiceAutoConfiguration全限定名为:
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
源码如下:
@Configuration @ConditionalOnClass(AuthenticationManager.class) @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnMissingBean({AuthenticationManager.class,AuthenticationProvider.class,UserDetailsService.class}) publicclassUserDetailsServiceAutoConfiguration{ privatestaticfinalStringNOOP_PASSWORD_PREFIX="{noop}"; privatestaticfinalPatternPASSWORD_ALGORITHM_PATTERN=Pattern.compile("^\\{.+}.*$"); privatestaticfinalLoglogger=LogFactory.getLog(UserDetailsServiceAutoConfiguration.class); @Bean @ConditionalOnMissingBean( type="org.springframework.security.oauth2.client.registration.ClientRegistrationRepository") @Lazy publicInMemoryUserDetailsManagerinMemoryUserDetailsManager(SecurityPropertiesproperties, ObjectProviderpasswordEncoder){ SecurityProperties.Useruser=properties.getUser(); List roles=user.getRoles(); returnnewInMemoryUserDetailsManager( User.withUsername(user.getName()).password(getOrDeducePassword(user,passwordEncoder.getIfAvailable())) .roles(StringUtils.toStringArray(roles)).build()); } privateStringgetOrDeducePassword(SecurityProperties.Useruser,PasswordEncoderencoder){ Stringpassword=user.getPassword(); if(user.isPasswordGenerated()){ logger.info(String.format("%n%nUsinggeneratedsecuritypassword:%s%n",user.getPassword())); } if(encoder!=null||PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()){ returnpassword; } returnNOOP_PASSWORD_PREFIX+password; } }
我们来简单解读一下该类,从@Conditional系列注解我们知道该类在类路径下存在AuthenticationManager、在Spring容器中存在BeanObjectPostProcessor并且不存在BeanAuthenticationManager,AuthenticationProvider,UserDetailsService的情况下生效。千万不要纠结这些类干嘛用的!该类只初始化了一个UserDetailsManager类型的Bean。UserDetailsManager类型负责对安全用户实体抽象UserDetails的增删查改操作。同时还继承了UserDetailsService接口。
明白了上面这些让我们把目光再回到UserDetailsServiceAutoConfiguration上来。该类初始化了一个名为InMemoryUserDetailsManager的内存用户管理器。该管理器通过配置注入了一个默认的UserDetails存在内存中,就是我们上面用的那个user,每次启动user都是动态生成的。那么问题来了如果我们定义自己的UserDetailsManagerBean是不是就可以实现我们需要的用户管理逻辑呢?
3.4自定义UserDetailsManager
我们来自定义一个UserDetailsManager来看看能不能达到自定义用户管理的效果。首先我们针对UserDetailsManager的所有方法进行一个代理的实现,我们依然将用户存在内存中,区别就是这是我们自定义的:
packagecn.felord.spring.security; importorg.springframework.security.access.AccessDeniedException; importorg.springframework.security.core.Authentication; importorg.springframework.security.core.context.SecurityContextHolder; importorg.springframework.security.core.userdetails.UserDetails; importorg.springframework.security.core.userdetails.UsernameNotFoundException; importjava.util.HashMap; importjava.util.Map; /** *代理{@linkorg.springframework.security.provisioning.UserDetailsManager}所有功能 * *@authorFelordcn */ publicclassUserDetailsRepository{ privateMapusers=newHashMap<>(); publicvoidcreateUser(UserDetailsuser){ users.putIfAbsent(user.getUsername(),user); } publicvoidupdateUser(UserDetailsuser){ users.put(user.getUsername(),user); } publicvoiddeleteUser(Stringusername){ users.remove(username); } publicvoidchangePassword(StringoldPassword,StringnewPassword){ AuthenticationcurrentUser=SecurityContextHolder.getContext() .getAuthentication(); if(currentUser==null){ //Thiswouldindicatebadcodingsomewhere thrownewAccessDeniedException( "Can'tchangepasswordasnoAuthenticationobjectfoundincontext" +"forcurrentuser."); } Stringusername=currentUser.getName(); UserDetailsuser=users.get(username); if(user==null){ thrownewIllegalStateException("Currentuserdoesn'texistindatabase."); } //todocopyInMemoryUserDetailsManager自行实现具体的更新密码逻辑 } publicbooleanuserExists(Stringusername){ returnusers.containsKey(username); } publicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{ returnusers.get(username); } }
该类负责具体对UserDetails的增删改查操作。我们将其注入Spring容器:
@Bean publicUserDetailsRepositoryuserDetailsRepository(){ UserDetailsRepositoryuserDetailsRepository=newUserDetailsRepository(); //为了让我们的登录能够运行这里我们初始化一个用户Felordcn密码采用明文当你在密码12345上使用了前缀{noop}意味着你的密码不使用加密,authorities一定不能为空这代表用户的角色权限集合 UserDetailsfelordcn=User.withUsername("Felordcn").password("{noop}12345").authorities(AuthorityUtils.NO_AUTHORITIES).build(); userDetailsRepository.createUser(felordcn); returnuserDetailsRepository; }
为了方便测试我们也内置一个名称为Felordcn密码为12345的UserDetails用户,密码采用明文当你在密码12345上使用了前缀{noop}意味着你的密码不使用加密,这里我们并没有指定密码加密方式你可以使用PasswordEncoder来指定一种加密方式。通常推荐使用Bcrypt作为加密方式。默认SpringSecurity使用的也是此方式。authorities一定不能为null这代表用户的角色权限集合。接下来我们实现一个UserDetailsManager并注入Spring容器:
@Bean publicUserDetailsManageruserDetailsManager(UserDetailsRepositoryuserDetailsRepository){ returnnewUserDetailsManager(){ @Override publicvoidcreateUser(UserDetailsuser){ userDetailsRepository.createUser(user); } @Override publicvoidupdateUser(UserDetailsuser){ userDetailsRepository.updateUser(user); } @Override publicvoiddeleteUser(Stringusername){ userDetailsRepository.deleteUser(username); } @Override publicvoidchangePassword(StringoldPassword,StringnewPassword){ userDetailsRepository.changePassword(oldPassword,newPassword); } @Override publicbooleanuserExists(Stringusername){ returnuserDetailsRepository.userExists(username); } @Override publicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{ returnuserDetailsRepository.loadUserByUsername(username); } }; }
这样实际执行委托给了UserDetailsRepository来做。我们重复章节3.的动作进入登陆页面分别输入Felordcn和12345成功进入。
3.5数据库管理用户
经过以上的配置,相信聪明的你已经知道如何使用数据库来管理用户了。只需要将UserDetailsRepository中的users属性替代为抽象的Dao接口就行了,无论你使用Jpa还是Mybatis来实现。
4.总结
今天我们对SpringSecurity中的用户信息UserDetails相关进行的一些解读。并自定义了用户信息处理服务。相信你已经对在SpringSecurity中如何加载用户信息,如何扩展用户信息有所掌握了。后面我们会由浅入深慢慢解读SpringSecurity。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。