一、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、比较密码代码:

二、自定义登录页

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

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

看以下代码:

@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也只是空对象,因此在创建的时候加一些属性
    }

}

我就不从源码分析了,我们只需要知道

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

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

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

在resources目录下创建 templates 目录,定义 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

@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这里转悠。

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

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

如图:

然后测试发现还是不行。

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

这样就好了

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

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

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

@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,其他名字也可以

/**
 * 用户表 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接口的子接口

子接口Authentication还有对应的实现类:UsernamePasswordAuthenticationToken

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

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

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

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

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

4.1 获取用户信息优化

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

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

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

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

这样是否可行呢?

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

@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文件中配置:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐