Spring Boot 2 + Spring Security 5 自定义登录验证(3)

Java源码网 5月前 ⋅ 440 阅读

Spring Security.jpg

前言

上篇文章介绍了如何在Spring Boot引入Security

接下来,博主会简单的介绍下如何自定义登录配置

开始

首先,我们要有数据库的用户表,这里我用的是mysql5.6 表结构如下:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 01.png

字段的话就不详细介绍了,相信看名字就能懂

整体demo结构如图:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 02.png

虽然说是demo,但是本着严格务实的态度,也是遵守MVC的调用流程,所以包可能会有点繁琐

这里简单的说下这个登录验证的流程,以便大家更好的理解下面的代码,先看图:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 03.png

绿色背景色为自定义实现的,也就是下面会出现的类方法 对于中间件那块来说是暂时没有的,可以不管先,后面的文章会引入,到时候再作介绍

当然,Spring Security认证的流程是没有那么简单的,这里只是给大家方便理解才简化了很多流程

下面开始展示代码

由于需要操作数据库,以及展示页面等,小编这里就需要引入持久层以及前端页面一些框架 这里博主用的是Spring Data Jpa,前端用的是Thymeleaf,Maven代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.7.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.javaymw</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.58</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

添加完相关的依赖,还是到项目的根目录下,执行maven的编译命令,把相关的jar下载下来:mvn clean compile

yml的配置不需要多大的修改,这次只是配置了数据源和jpa的一些基础属性,代码如下:

server:
  tomcat:
  uri-encoding: UTF-8
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: root
  jpa:
    database: MYSQL
    show-sql: true
    hibernate:
      ddl-auto: update
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

这里要注意的是,当引入的mysql-connector-java版本是6.0以上的话,那驱动就是:

com.mysql.cj.jdbc.Driver

中间是多个cj的 还有就是在数据源url后面要加上serverTimezone=UTC这条参数,否则也是会报错的

jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC

接下来就是编写spring security的配置类:

SecurityConfig .java

/**
 * Copyright 2020. javaymw.com Studio All Right Reserved
 * <p>
 * Create on 2020-06-05 21:58
 * Created by zhaoxinguo
 * Version 2.0.0
 */
package com.javaymw.demo.config;

import com.javaymw.demo.core.LoginValidateAuthenticationProvider;
import com.javaymw.demo.core.handler.LoginFailureHandler;
import com.javaymw.demo.core.handler.LoginSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

/**
 * @description: Spring Security 核心配置类
 * @author zhaoxinguo
 * @date 2020/6/5 21:57
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //自定义认证
    @Resource
    private LoginValidateAuthenticationProvider loginValidateAuthenticationProvider;

    //登录成功handler
    @Resource
    private LoginSuccessHandler loginSuccessHandler;

    //登录失败handler
    @Resource
    private LoginFailureHandler loginFailureHandler;

    /**
     * 权限核心配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //基础设置
        http.httpBasic()//配置HTTP基本身份验证
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()//所有请求都需要认证
                .and()
                .formLogin() //登录表单
                .loginPage("/login")//登录页面url
                .loginProcessingUrl("/login")//登录验证url
                .defaultSuccessUrl("/index")//成功登录跳转
                .successHandler(loginSuccessHandler)//成功登录处理器
                .failureHandler(loginFailureHandler)//失败登录处理器
                .permitAll();//登录成功后有权限访问所有页面
        //关闭csrf跨域攻击防御
        http.csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //这里要设置自定义认证
        auth.authenticationProvider(loginValidateAuthenticationProvider);
    }

    /**
     * BCrypt加密
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

注意,Spring Security配置类必须继承WebSecurityConfigurerAdapter类才会生效

这里BCrypt加密方式是官方推荐使用的,还有就是Spring Security5.x是不需要配置加密方式的,因为它可以匹配多种加密方式以用来解密,只需要在密码前面加上加密方式即可,格式如下:

{加密方式}密文

例如:

  • {MD5}e10adc3949ba59abbe56e057f20f883e
  • {bcrypt}$2a101010bOZ5qFQS4OojeLUdb6K8.OU/KrVR8vzdo7QaCNKNG4oaIYUrAGKJ2

这样就可以实现兼容多个加密方式,可以说是挺人性化的,不过我这里还是规定死了哈哈哈哈

然后就是编写User实体类和UserService实现类:

User.java

/**
 * Copyright 2020. javaymw.com Studio All Right Reserved
 * <p>
 * Create on 2020-06-05 22:01
 * Created by zhaoxinguo
 * Version 2.0.0
 */
package com.javaymw.demo.sys.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;

/**
 * @description: TODO
 * @author zhaoxinguo
 * @date 2020/6/5 22:01
 */
@Entity
@Table(name = "sys_user")
public class User implements UserDetails {

    //id
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    protected Integer id;

    //用户名
    @Column
    private String username;

    //密码
    @Column(nullable = false)
    private String password;

    /**
     * 是否锁定
     * true: 未锁定
     * false: 锁定
     */
    @Column
    private boolean lockedFlag;

    //security存储权限认证用的
    @Transient
    private Collection<? extends GrantedAuthority> authorities;

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

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    /**
     * 用户账号是否过期
     * true: 未过期
     * false: 已过期
     *
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 用户账号是否被锁定
     * true: 未锁定
     * false: 锁定
     *
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return lockedFlag;
    }

    /**
     * 用户账号凭证(密码)是否过期
     * 简单的说就是可能会因为修改了密码导致凭证过期这样的场景
     * true: 过期
     * false: 无效
     *
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 用户账号是否被启用
     * true: 启用
     * false: 未启用
     *
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public boolean isLockedFlag() {
        return lockedFlag;
    }

    public void setLockedFlag(boolean lockedFlag) {
        this.lockedFlag = lockedFlag;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }
}

这里要说明下,UserDetails是Spring Security提供的一个保存用户账号信息的接口,详情请看代码注释,因为有些地方是没有用到的,所以就写死了很多属性,大家可根据实际需求来修改使用

UserService.java

/**
 * Copyright 2020. javaymw.com Studio All Right Reserved
 * <p>
 * Create on 2020-06-05 22:04
 * Created by zhaoxinguo
 * Version 2.0.0
 */
package com.javaymw.demo.sys.service;

import com.javaymw.demo.sys.entity.User;
import com.javaymw.demo.sys.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

/**
 * @description: TODO
 * @author zhaoxinguo
 * @date 2020/6/5 22:04
 */
@Service
public class UserService implements UserDetailsService {

    @Resource
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("不存在该用户!");
        }
        return user;
    }
}

同理 UserDetailsService 也是是spring security提供的,这里实现了加载用户名称的方法,目的是为了获取用户信息,以便接下来的认证

UserRepository .java

/**
 * Copyright 2020. javaymw.com Studio All Right Reserved
 * <p>
 * Create on 2020-06-05 22:03
 * Created by zhaoxinguo
 * Version 2.0.0
 */
package com.javaymw.demo.sys.repository;

import com.javaymw.demo.sys.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * @description: TODO
 * @author zhaoxinguo
 * @date 2020/6/5 22:03
 */
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

    User findUserByUsername(String username);
}

这个相信不用多说了吧

下面就是自定义认证的核心代码:

LoginValidateAuthenticationProvider.java

/**
 * Copyright 2020. javaymw.com Studio All Right Reserved
 * <p>
 * Create on 2020-06-05 21:59
 * Created by zhaoxinguo
 * Version 2.0.0
 */
package com.javaymw.demo.core;

import com.javaymw.demo.sys.entity.User;
import com.javaymw.demo.sys.service.UserService;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

/**
 * @description: 自定义认证核心类
 * @author zhaoxinguo
 * @date 2020/6/5 21:59
 */
@Component
public class LoginValidateAuthenticationProvider implements AuthenticationProvider {

    @Resource
    private UserService userService;

    /**
     * 解密用的
     */
    @Resource
    private PasswordEncoder passwordEncoder;

    /**
     * 进行身份验证
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //获取输入的用户名
        String username = authentication.getName();
        //获取输入的明文
        String rawPassword = (String) authentication.getCredentials();
        //查询用户是否存在
        User user = (User) userService.loadUserByUsername(username);
        if (!user.isEnabled()) {
            throw new DisabledException("该账户已被禁用,请联系管理员");
        } else if (!user.isAccountNonLocked()) {
            throw new LockedException("该账号已被锁定");
        } else if (!user.isAccountNonExpired()) {
            throw new AccountExpiredException("该账号已过期,请联系管理员");
        } else if (!user.isCredentialsNonExpired()) {
            throw new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录");
        }
        //验证密码
        if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
            throw new BadCredentialsException("输入密码错误!");
        }
        return new UsernamePasswordAuthenticationToken(user, rawPassword, user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //确保authentication能转成该类
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

这里通过实现AuthenticationProvider 认证授权类,以达到自定义登录的效果,注意,这里是结合了之前实现的loadUserByUsername方法去获取用户信息,以及用户状态去判断登录是否能通过

接下来就是handler代码:

LoginSuccessHandler.java

/**
 * Copyright 2020. javaymw.com Studio All Right Reserved
 * <p>
 * Create on 2020-06-05 22:01
 * Created by zhaoxinguo
 * Version 2.0.0
 */
package com.javaymw.demo.core.handler;

import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * @description: 登陆成功处理handler
 * @author zhaoxinguo
 * @date 2020/6/5 22:00
 */
@Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        //登录成功返回
        Map<String, Object> paramMap = new HashMap<>(2);
        paramMap.put("code", "200");
        paramMap.put("message", "登录成功!");
        //设置返回请求头
        response.setContentType("application/json;charset=utf-8");
        //写出流
        PrintWriter out = response.getWriter();
        out.write(JSONObject.toJSONString(paramMap));
        out.flush();
        out.close();
    }
}

LoginFailureHandler.java

/**
 * Copyright 2020. javaymw.com Studio All Right Reserved
 * <p>
 * Create on 2020-06-05 22:00
 * Created by zhaoxinguo
 * Version 2.0.0
 */
package com.javaymw.demo.core.handler;

import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * @description: 登录失败处理handler
 * @author zhaoxinguo
 * @date 2020/6/5 22:00
 */
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        //登录失败信息返回
        Map<String, Object> paramMap = new HashMap<>(2);
        paramMap.put("code", "500");
        paramMap.put("message", exception.getMessage());
        //设置返回请求头
        response.setContentType("application/json;charset=utf-8");
        //写出流
        PrintWriter out = response.getWriter();
        out.write(JSONObject.toJSONString(paramMap));
        out.flush();
        out.close();
    }
}

那么到这里,也已经差不多了,现在还差的是登录的前端页面和一些效果

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
<h2>登录页</h2>
<form id="loginForm" action="/login" method="post">
    用户名:<input type="text" id="username" name="username"><br/><br/>
    密&nbsp;&nbsp;&nbsp;码:<input type="password" id="password" name="password"><br/><br/>
    <button id="loginBtn" type="button">登录</button>
</form>


    $("#loginBtn").click(function () {
        $.ajax({
            type: "POST",
            url: "/login",
            data: $("#loginForm").serialize(),
            dataType: "JSON",
            success: function (data) {
                console.log(data);
                //window.location.href = "/index";
            }
        });
    });

</body>
</html>

这里为了方便演示,就直接在前端输出登录信息,下面看看演示图:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 04.png

这里我在数据库加了条登录数据,数据默认用户脚本如下,用户名、密码(admin/123456):

INSERT INTO `test`.`sys_user` (`id`, `locked_flag`, `password`, `username`) VALUES ('1', b'1', '$2a$10$UNVzCpRC3ND2XrCu8rZWJ.OkAzpyP651itODJiKTMOpqLMWrTLcEi', 'admin');

这里我在数据库加了条登录数据,当填正确账号点击登录的时候,显示是成功的

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 05.png

随便输入个错误的密码则是:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 06.png

输入个不存在的用户名则是:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 07.png

修改用户状态为锁定则是:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 08.png

那么基本代码和效果也演示完毕了

源码获取方式加加入QQ交流群(715224124),进群找群主要源码,如果有问题,可以提出疑问,群主会尽量帮助解决~

希望能帮助到大家,如果有不好或者错误的地方希望能多多提出,谢谢大家~


全部评论: 0

    我有话说: