SpringSecurity(二)

一、Security数据库登录流程分析

1、访问http://localhost:8080/add

2、被spring security的filter过滤器拦截(里面有16个Filter);

3、由于没有登录过,所以spring security就跳转到登录页(登录页是框架生成的)

4、我们在登录页输入账号和密码去登录提交;(账号和密码是数据库的账号密码)

5、spring security里面的UsernamePasswordAuthenticationFilter接收账号和密码;

6、第5步的这个filter会调用loadUserByUsername(String username)方法去数据库查询用户;

7、从数据库查询到用户后,把用户组装成UserDetails对象,然后返回给SpringSecurity框架;

8、第7步返回后,再回到框架的filter里面进行用户状态的判断,用户对象中默认有4个状态字段,如果这4个状态字段的值都是true,该用户才能登录,否则就是提示用户状态不正常,不能登录的(框架中实际上只判断3个状态值,那个密码是否过期没有做判断);

9、第7步返回后,再回到框架的filter里面进行密码的匹配,如果密码匹配上了,就登录成功,否则失败;

10、比较密码代码:

AbstractUserDetailsAuthenticationProvider.java

java 复制代码
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
        return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
    });
    String username = this.determineUsername(authentication);
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        cacheWasUsed = false;

        try {
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        } catch (UsernameNotFoundException var6) {
            UsernameNotFoundException ex = var6;
            this.logger.debug(LogMessage.format("Failed to find user '%s'", username));
            if (!this.hideUserNotFoundExceptions) {
                throw ex;
            }

            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }

        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }

    try {
        this.preAuthenticationChecks.check(user);
        // 是在自定义的UserDetailsService实现中用来执行额外的身份验证检查的
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    } catch (AuthenticationException var7) {
        AuthenticationException ex = var7;
        if (!cacheWasUsed) {
            throw ex;
        }

        cacheWasUsed = false;
        user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        this.preAuthenticationChecks.check(user);
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    }

    this.postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }

    return this.createSuccessAuthentication(principalToReturn, authentication, user);
}

上述源码中additionalAuthenticationChecks方法是在自定义的UserDetailsService实现中用来执行额外的身份验证检查的。这个方法允许开发者根据业务需求对用户信息进行额外的验证。

通过当前方法定位到 protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;抽象方法

DaoAuthenticationProvider.java

java 复制代码
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        this.logger.debug("Failed to authenticate since no credentials provided");
        throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    } else {
        String presentedPassword = authentication.getCredentials().toString();
        // 这里是验证密码正确的关键步骤   判断密码是否正确,如果不正确则抛出异常  
        if (!((PasswordEncoder)this.passwordEncoder.get()).matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
}

解析:

该代码块就是进行前端传过来的密码 和 数据库的查询的密码 进行匹配,如果匹配失败,意味着你输入的密码与系统中存储的密码不匹配.

二、自定义登录页

如果以后我们想要在登录页上加一些其他的东西去登录,比如验证码该怎么办呢?对吧,因此我们想能不能自己定义登录也做这个事情,及有所求,必有所应。

我们需要在Security的配置类中再注册一个Bean(SecurityFilterChain),安全过滤器链实现,在容器生成该Bean的时候呢,加入我们自己的业务逻辑就可以了。

看以下代码:

SecurityConfig.java

java 复制代码
@Configuration
public class SecurityConfig {

    // 配置加密器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    // 注册一个Bean(安全过滤器链), 定义一些我们自己的过滤逻辑(认证登录的时候去访问我们自定义的登录页面)
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                // 发送/toLogin请求  跳转到登录页面 ==>  页面的选择: jsp(淘汰),vue(暂时不考虑)  html(thymeleaf暂时使用) 需要引入依赖
                .formLogin(formLogin -> formLogin.loginPage("/toLogin"))
                .build(); // 如果只写一个build也只是空对象,因此在创建的时候加一些属性
    }

}

源码分析:

在Spring Security中,FormLoginConfigurer 是一个用于配置表单登录的类。它允许你自定义登录表单的行为,比如登录页面URL、登录处理URL、登录成功或失败后的行为等。

通过源码分析可知Customizer接口上有一个注解@FunctionalInterface,‌表名当前接口属于函数式接口,‌函数式是Java中一种特殊的接口类型,‌有且仅有一个抽象方法‌,用于支持函数式编程和Lambda表达式简化代码实现。‌‌ 所以可以在return httpSecurity.formLogin()方法中编写Lambda表达式用来生成登录页面。

接下来我们去编写我们的login.html页面去,前提是先添加thymeleaf依赖:

XML 复制代码
<!--thymeleaf依赖   模版依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
  <version>3.5.3</version>
</dependency>

在resources目录下创建 templates 目录,定义 login.html 登录页

login.html

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
    <form method="post" action="/login">
        <p>
            username: <input name = "username">
        </p>
        <p>
            password: <input type="password" name="password"/>
        </p>
        <p>
            <input type="submit" value="登录"/>
        </p>
    </form>
</body>
</html>

接下来为了能访问到我们自定义的登录页,去编写一个去登录页面的controller

UserController.java

java 复制代码
@Controller
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 跳转登录页面
     * @return
     */
    @GetMapping("/toLogin")
    public String toLogin(){
        return "login";
    }

    /**
     * 用户登录功能
     * @param username
     * @param password
     * @return
     */
    @GetMapping("/login")
    @ResponseBody
    public String login(String username, String password){
        return "we are comming login";
    }

    @GetMapping("/add")
    @ResponseBody
    public String add(String username, String password){
        return "hello add...";
    }
}

特别注意:这个时候不能让该方法返回json,因为我们这里返回的这个字符串其实是代表的是去的页面,因此应该将 注解改为@Controller,在需要返回json的方法上添加ResponseBody注解就行。

来我们测试一下:发送toLogin请求能否去登录页面呢? ==> 肯定没问题

访问我们之前的 http://localhost:8080/add 路径,看看需不需要登录

此时发现不需要登录也能去访问添加功能,这样就没有了Security的效果了吗

因为现在走的是我们自己定义的过滤器链,而我们这里没有定义验证,框架定义了.

我们这里需要定义下就好了

再试一试

此时发现网页进不去了

我们来找原因:

拦截是所有请求都要验证是否认证,但是我们还没有登录认证,所以我们就会一直在/toLogin这里转悠。

知道原因就好解决了。怎么办呢?

我们只需要将 登录页面的请求 不让它认证不就可以了吗? 如何实现呢? 如下:

来吧,再验证一下吧:

先去访问/add请求,看会不会去验证来

但是,我们输入正确的用户名和密码之后还是不行:

这个是为啥呢?我们去看一下原来Security帮我们生成的页面,我们先把我们注册的那个安全过滤器链先注释一下看看:

右键查看源代码,我们发现登录页面上除了用用户名和密码外还有一个隐藏域:

我们只需要把这个字段加上去和相关的值也赋值过去就好了,值是Security框架生成的,通过thymeleaf语法获取一下就好了。

如图:

测试发现还是不行

当发送请求时,因为过滤器是自己写的,我们要告诉Spring Security安全框架,让我们自定义页面中的请求地址走安全框架进行认证。

此时发现可以进去了

三、获取当前登录用户信息

如果我们登录成功,完成认证后,如何获取用户的信息呢,对吧!

这个呢,我们也有相应的方式去实现,首先,在controller中定义一个成功登陆后的一个行为,比如:

UserController.java

java 复制代码
@Controller
public class UserController {
    
    @RequestMapping("/index")
    @ResponseBody
    public Principal index(Principal principal){
        return principal;
    }
    
}

Principal:源码注释内容:

This interface represents the abstract notion of a principal, which can be used to represent any entity, such as an individual, a corporation, and a login id. 啥意思呢?

该接口表示主体的抽象概念,可用于表示任何实体,例如个人、公司和登录 ID。

注意:这里的请求映射要用RequestMapping或者PostMapping,为什么呢?一会揭晓。

但是我们怎么让 security 知道我们认证成功后要发送/index请求呢?

那么就需要我们在security的配置类中做一个定制的配置就好了,告诉Security,怎么做呢?

这个代码的意思就是认证成功后转发的路径。

运行项目的结果:虽然能够返回用户信息,但是有效信息不多。

我们发现以上的结果包含了sessionid,用户名,和权限信息(目前是空权限,因为我们配置的就是空权限).

还有4个值,分别表示:

  1. 账户没有过期
  2. 账户没有被锁
  3. 凭证没有过期
  4. 可以使用

但是关于用户的信息,除了用户名之外什么都没有了。

但是我需要用户的完整信息应该怎么获取呢?

我们发现返回的信息标注的内容很眼熟,哪里见过呢?UserDetails的内容看看:

其实就是这个UserDetail的内容,但是我们发现也不包含我们想要的信息。那怎么办呢?

开动一下脑筋,异想天开一下:我们的User实体能不能去实现UserDetails接口呢?

答案是显而易见的,这样我们只需要重写接口中的抽象方法就好了,当然默认方法想重写也不是不可以。

那么就开始改造吧:(为了方便操作,我们把我们自己的User改个名字叫Tuser,其他名字也可以

TUser.java

java 复制代码
/**
 * 用户表 user
 */
@Data
public class TUser implements UserDetails, Serializable {
    /**
     * 用户ID
     */
    private Integer id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 真实姓名
     */
    private String realName;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 状态:0-禁用,1-正常
     */
    private Byte status;

    /**
     * 创建时间
     */
    private Date createTime;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }
}

重写的东西是关于权限的内容,我们这里暂时先不动,实现了不报错就行。

那么紧接着我们自定义的UserService那里就可以改一下了

这里我们是不是返回我们自己的TUser就可以了呢?

因为TUser是UserDetails的实现类。因此可以直接返回TUser对象即可

那么就可以这么改了:

那么重新测试看看

四、用户信息存储组件修改

其实Controller中的Principal也可以修改,因为它是一个接口,那么我们来看看它的关系:

快捷键: ctrl + h 可以获取当前Principal接口的子接口

那么Principal也可以换成Authentication/UsernamePasswordAuthenticationToken都可以获取到用户信息。

如下: 这里先改成标记的方式请求映射,方便测试

此时发现以上两种情况都是可以获取到当前用户的详细信息:

或者通过框架上下文的方式也可以获取

这个时候在回头看我们之前说的security框架中的组件就能理解它们的意思了

4.1 获取用户信息优化

上图方法中的SecurityContextHolder.getContext().getAuthentication();内容比较长。

我们可不可以去写一个工具类呢?

应该可以的:添加 utils 包 / LoginInfoUtils

LoginInfoUtils.java

java 复制代码
public class LoginInfoUtils {
    public static TUser currentLoginUser(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        TUser tuser = (TUser) authentication.getPrincipal();
        return tuser;
    }
}

这样是否可行呢?

那么在Controller中就可以这么用了(不需要依赖注入的方式了):

UserController.java

java 复制代码
@Controller
public class UserController {

    @RequestMapping ("/index4")
    @ResponseBody
    public TUser index4(){
        return LoginInfoUtils.currentLoginUser();
    }

}

测试结果如下:

4.2 JSON日期处理扩展

小扩展:我们需要注意,密码不应该返回的,对吧,那么我们的处理方案有:写一个BO/VO,

这里说一个Json的注解,如果没有BO/VO可以使用如下的注解:

json日期格式化:记得加时区,不然数据库中的时间和返回的时间不一致,默认是0时区,我们这里是东8区。

测试结果如下:

但是你想一下,如果以后其他类中也有多个日期的属性,你都要这样一个一个去加格式化日期,这样方便吗?

肯定不行的,我们可以统一进行处理。

因为在springboot中默认的json格式化是由jackson处理的,我们可以将统一的日期格式在yml文件中配置:

相关推荐
Francek Chen2 小时前
【大数据存储与管理】NoSQL数据库:01 NoSQL简介
大数据·数据库·分布式·nosql
Database_Cool_3 小时前
【无标题】
数据库·阿里云·ai
isNotNullX3 小时前
BI如何落地?BI平台如何搭建?
大数据·数据库·人工智能
Shely20173 小时前
单表查询
数据库
5G丶3 小时前
ThinkPHP 集群部署完整指南
数据库·php
刘~浪地球3 小时前
数据库与缓存--MySQL 高可用架构设计
数据库·mysql·缓存
知识分享小能手3 小时前
MongoDB入门学习教程,从入门到精通,MongoDB的了解应用程序的动态(18)
数据库·学习·mongodb
oradh3 小时前
Oracle数据类型概述(一)
数据库·oracle·oracle基础·oracle入门基础·oracle数据类型