美文网首页
Spring Boot Security5 自定义登录验证(3)

Spring Boot Security5 自定义登录验证(3)

作者: Ceruleano | 来源:发表于2019-08-15 21:16 被阅读0次

    前言

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

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

    开始

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

    image.png

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

    整体demo结构如图:


    在这里插入图片描述

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

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

    image.png

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

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

    下面开始展示代码

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

      <dependencies>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</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-web</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>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <!-- 阿里json框架 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.58</version>
            </dependency>
    
        </dependencies>
    

    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: 940806
    
      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

    package com.demo.ssdemo.config;
    
    import com.demo.ssdemo.core.LoginValidateAuthenticationProvider;
    import com.demo.ssdemo.core.handler.LoginFailureHandler;
    import com.demo.ssdemo.core.handler.LoginSuccessHandler;
    import com.demo.ssdemo.sys.service.UserService;
    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.method.configuration.EnableGlobalMethodSecurity;
    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;
    
    @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}2a10$bOZ5qFQS4OojeLUdb6K8.OU/KrVR8vzdo7QaCNKNG4oaIYUrAGKJ2

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

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

    User.java

    package com.demo.ssdemo.sys.entity;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import javax.persistence.*;
    import java.util.Collection;
    
    @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;
        }
    
        ...get、set方法省略...
        
    }
    

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

    UserService.java

    package com.demo.ssdemo.sys.service;
    
    import com.demo.ssdemo.sys.entity.User;
    import com.demo.ssdemo.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;
    
    @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

    package com.demo.ssdemo.sys.repository;
    
    import com.demo.ssdemo.sys.entity.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public interface UserRepository extends JpaRepository<User, Integer> {
    
        User findUserByUsername(String username);
    
    }
    

    这个相信不用多说了吧

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

    LoginValidateAuthenticationProvider.java

    package com.demo.ssdemo.core;
    
    import com.demo.ssdemo.sys.entity.User;
    import com.demo.ssdemo.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;
    
    @Component
    public class LoginValidateAuthenticationProvider implements AuthenticationProvider {
    
        @Resource
        private UserService userService;
    
        //解密用的
        @Resource
        private PasswordEncoder passwordEncoder;
    
        @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

    package com.demo.ssdemo.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;
    
    /**
     * 登陆成功处理handler
     */
    @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<>();
            paramMap.put("code", "400");
            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

    package com.demo.ssdemo.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;
    
    /**
     *  登陆失败处理handler
     **/
    @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<>();
            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>
    
    <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
    <script type="text/javascript">
    
        $("#loginBtn").click(function () {
            $.ajax({
                type: "POST",
                url: "/login",
                data: $("#loginForm").serialize(),
                dataType: "JSON",
                success: function (data) {
                    console.log(data);
                    //window.location.href = "/index";
                }
            });
    
        });
    
    </script>
    </body>
    </html>
    

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


    image.png

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


    image.png

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


    image.png

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


    image.png

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

    demo也已经放到github,获取方式在文章的Spring Boot2 + Spring Security5 系列搭建教程开头篇(1) 结尾处

    如果小伙伴遇到什么问题,或者哪里不明白欢迎评论或私信,也可以在公众号里面私信问都可以,谢谢大家~

    相关文章

      网友评论

          本文标题:Spring Boot Security5 自定义登录验证(3)

          本文链接:https://www.haomeiwen.com/subject/kkitsctx.html