最新公告
  • 新注册用户请前往个人中心绑定邮箱以便接收相关凭证邮件!!!点击前往个人中心
  • 一起搞清楚 Spring Security 中的 UserDetails

    1. 前言

    前一篇介绍了 Spring Security 入门的基础准备。从今天开始我们来一步步窥探它是如何工作的。我们又该如何驾驭它。请多多关注公众号: Felordcn 。本篇将通过 Spring Boot 2.x 来讲解 Spring Security 中的用户主体UserDetails以及从中找点乐子。

    2. Spring Boot 集成 Spring Security

    这个简直老生常谈了。不过为了照顾大多数还是说一下。集成 Spring Security 只需要引入其对应的 Starter 组件。Spring Security 不仅仅能保护Servlet Web 应用,也可以保护Reactive Web应用,本文我们讲前者。我们只需要在 Spring Security 项目引入以下依赖即可:
    <dependencies>
    <!-- actuator 指标监控 非必须 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- spring security starter 必须 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- spring mvc servlet web 必须 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- lombok 插件 非必须 -->
    <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>

    3. UserDetailsServiceAutoConfiguration

    启动项目,访问Actuator端点http://localhost:8080/actuator会跳转到一个登录页面http://localhost:8080/login如下:
    要求你输入用户名 Username (默认值为user)和密码 Password 。密码在springboot控制台会打印出类似 Using generated security password: e1f163be-ad18-4be1-977c-88a6bcee0d37 的字样,后面的长串就是密码,当然这不是生产可用的。如果你足够细心会从控制台打印日志发现该随机密码是由UserDetailsServiceAutoConfiguration 配置类生成的,我们就从它开始顺藤摸瓜来一探究竟。

    3.1 UserDetailsService

    UserDetailsService接口。该接口只提供了一个方法:
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
    该方法很容易理解:通过用户名来加载用户 。这个方法主要用于从系统数据中查询并加载具体的用户到Spring Security中。

    3.2 UserDetails

    从上面UserDetailsService 可以知道最终交给Spring Security的是UserDetails 。该接口是提供用户信息的核心接口。该接口实现仅仅存储用户的信息。后续会将该接口提供的用户信息封装到认证对象Authentication中去。UserDetails 默认提供了:
    • 用户的权限集, 默认需要添加ROLE_ 前缀
    • 用户的加密后的密码, 不加密会使用{noop}前缀
    • 应用内唯一的用户名
    • 账户是否过期
    • 账户是否锁定
    • 凭证是否过期
    • 用户是否可用
    如果以上的信息满足不了你使用,你可以自行实现扩展以存储更多的用户信息。比如用户的邮箱、手机号等等。通常我们使用其实现类:
    org.springframework.security.core.userdetails.User
    该类内置一个建造器UserBuilder 会很方便地帮助我们构建UserDetails 对象,后面我们会用到它。

    3.3 UserDetailsServiceAutoConfiguration

    UserDetailsServiceAutoConfiguration 全限定名为:
    org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
    源码如下:
    @Configuration
    @ConditionalOnClass(AuthenticationManager.class)
    @ConditionalOnBean(ObjectPostProcessor.class)
    @ConditionalOnMissingBean({ AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class })
    publicclassUserDetailsServiceAutoConfiguration {
    privatestaticfinal String NOOP_PASSWORD_PREFIX = "{noop}";
    privatestaticfinal Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\{.+}.*$");
    privatestaticfinal 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();
    returnnew 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;
    }
    }
    我们来简单解读一下该类,从@Conditional系列注解我们知道该类在类路径下存在AuthenticationManager、在Spring 容器中存在Bean ObjectPostProcessor并且不存在Bean AuthenticationManagerAuthenticationProviderUserDetailsService的情况下生效。千万不要纠结这些类干嘛用的! 该类只初始化了一个UserDetailsManager 类型的Bean。UserDetailsManager 类型负责对安全用户实体抽象UserDetails的增删查改操作。同时还继承了UserDetailsService接口。
    明白了上面这些让我们把目光再回到UserDetailsServiceAutoConfiguration 上来。该类初始化了一个名为InMemoryUserDetailsManager 的内存用户管理器。该管理器通过配置注入了一个默认的UserDetails存在内存中,就是我们上面用的那个user ,每次启动user都是动态生成的。那么问题来了如果我们定义自己的UserDetailsManager Bean是不是就可以实现我们需要的用户管理逻辑呢?

    3.4 自定义UserDetailsManager

    我们来自定义一个UserDetailsManager 来看看能不能达到自定义用户管理的效果。首先我们针对UserDetailsManager 的所有方法进行一个代理的实现,我们依然将用户存在内存中,区别就是这是我们自定义的:
    package cn.felord.spring.security;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import java.util.HashMap;
    import java.util.Map;
    /**
    * 代理 {@link org.springframework.security.provisioning.UserDetailsManager} 所有功能
    *
    * @author Felordcn
    */
    publicclassUserDetailsRepository {
    private Map<String, UserDetails> users = new HashMap<>();
    publicvoidcreateUser(UserDetails user) {
    users.putIfAbsent(user.getUsername(), user);
    }
    publicvoidupdateUser(UserDetails user) {
    users.put(user.getUsername(), user);
    }
    publicvoiddeleteUser(String username) {
    users.remove(username);
    }
    publicvoidchangePassword(String oldPassword, String newPassword) {
    Authentication currentUser = SecurityContextHolder.getContext()
    .getAuthentication();
    if (currentUser == null) {
    // This would indicate bad coding somewhere
    thrownew AccessDeniedException(
    "Can't change password as no Authentication object found in context "
    + "for current user.");
    }
    String username = currentUser.getName();
    UserDetails user = users.get(username);
    if (user == null) {
    thrownew IllegalStateException("Current user doesn't exist in database.");
    }
    // todo copy InMemoryUserDetailsManager 自行实现具体的更新密码逻辑
    }
    publicbooleanuserExists(String username) {
    return users.containsKey(username);
    }
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return users.get(username);
    }
    }
    该类负责具体对UserDetails 的增删改查操作。我们将其注入Spring 容器:
    @Bean
    public UserDetailsRepository userDetailsRepository() {
    UserDetailsRepository userDetailsRepository = new UserDetailsRepository();
    // 为了让我们的登录能够运行 这里我们初始化一个用户Felordcn 密码采用明文 当你在密码12345上使用了前缀{noop} 意味着你的密码不使用加密,authorities 一定不能为空 这代表用户的角色权限集合
    UserDetails felordcn = User.withUsername("Felordcn").password("{noop}12345").authorities(AuthorityUtils.NO_AUTHORITIES).build();
    userDetailsRepository.createUser(felordcn);
    return userDetailsRepository;
    }
    为了方便测试 我们也内置一个名称为Felordcn 密码为12345UserDetails用户,密码采用明文 当你在密码12345上使用了前缀{noop} 意味着你的密码不使用加密,这里我们并没有指定密码加密方式你可以使用PasswordEncoder 来指定一种加密方式。通常推荐使用Bcrypt作为加密方式。默认Spring Security使用的也是此方式。authorities 一定不能为null 这代表用户的角色权限集合。接下来我们实现一个UserDetailsManager 并注入Spring 容器:
    @Bean
    public UserDetailsManager userDetailsManager(UserDetailsRepository userDetailsRepository) {
    returnnew UserDetailsManager() {
    @Override
    publicvoidcreateUser(UserDetails user) {
    userDetailsRepository.createUser(user);
    }
    @Override
    publicvoidupdateUser(UserDetails user) {
    userDetailsRepository.updateUser(user);
    }
    @Override
    publicvoiddeleteUser(String username) {
    userDetailsRepository.deleteUser(username);
    }
    @Override
    publicvoidchangePassword(String oldPassword, String newPassword) {
    userDetailsRepository.changePassword(oldPassword, newPassword);
    }
    @Override
    publicbooleanuserExists(String username) {
    return userDetailsRepository.userExists(username);
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return userDetailsRepository.loadUserByUsername(username);
    }
    };
    }
    这样实际执行委托给了UserDetailsRepository 来做。我们重复 章节3. 的动作进入登陆页面分别输入Felordcn12345 成功进入。

    3.5 数据库管理用户

    经过以上的配置,相信聪明的你已经知道如何使用数据库来管理用户了 。只需要将 UserDetailsRepository 中的 users 属性替代为抽象的Dao接口就行了,无论你使用Jpa还是Mybatis来实现。

    4. 总结

    今天我们对Spring Security 中的用户信息 UserDetails 相关进行的一些解读。并自定义了用户信息处理服务。相信你已经对在Spring Security中如何加载用户信息,如何扩展用户信息有所掌握了。后面我们会由浅入深慢慢解读Spring Security。相关代码已经上传git仓库,关注公众号Felordcn 后回复ss01 获取demo源码。 后续也可以及时获取更多相关干货教程。
    本站所有文章均由网友分享,仅用于参考学习用,请勿直接转载,如有侵权,请联系网站客服删除相关文章。若由于商用引起版权纠纷,一切责任均由使用者承担
    极客文库 » 一起搞清楚 Spring Security 中的 UserDetails

    常见问题FAQ

    如果资源链接失效了怎么办?
    本站用户分享的所有资源都有自动备份机制,如果资源链接失效,请联系本站客服QQ:2580505920更新资源地址。
    如果用户分享的资源与描述不符怎么办?
    可以联系客服QQ:2580505920,如果要求合理可以安排退款或者退赞助积分。
    如何分享个人资源获取赞助积分或其他奖励?
    本站用户可以分享自己的资源,但是必须保证资源没有侵权行为。点击个人中心,根据操作填写并上传即可。资源所获收益完全归属上传者,每周可申请提现一次。
    如果您发现了本资源有侵权行为怎么办?
    及时联系客服QQ:2580505920,核实予以删除。

    参与讨论

    • 125会员总数(位)
    • 3725资源总数(个)
    • 5本周发布(个)
    • 0 今日发布(个)
    • 295稳定运行(天)

    欢迎加入「极客文库」,成为原创作者从这里开始!

    立即加入 了解更多
    成为赞助用户享有更多特权立即升级