• SpringSecurity(六)【自定义认证案例】


    六、自定义认证案例


    6.1 传统web开发认证总结案例

    1. 创建一个spring-security-03模块

    2. 导入依赖pom.xml

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>
        
        <dependency>
            <groupId>org.thymeleaf.extrasgroupId>
            <artifactId>thymeleaf-extras-springsecurity5artifactId>
            <version>3.0.4.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.2.2version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.22version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.2.8version>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    1. application.yml配置文件
    # 端口号
    server:
      port: 3034
    # 服务应用名称
    spring:
      application:
        name: SpringSecurity02
      # 关闭thymeleaf缓存(用于修改完之后立即生效)
      thymeleaf:
        cache: false
        # thymeleaf默认配置
        prefix: classpath:/templates/
        suffix: .html
        encoding: UTF-8
        mode: HTML
      # 数据源
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
    mybatis:
      # 注意 mapper 映射文件必须使用"/"
      type-aliases-package: com.vinjcent.pojo
      mapper-locations: com/vinjcent/mapper/**/*.xml
    
    # 日志处理,为了展示 mybatis 运行 sql 语句
    logging:
      level:
        com:
          vinjcent:
            debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    1. 编写实体类User、Role
    • User
    package com.vinjcent.pojo;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.*;
    
    // 自定义用户User
    public class User implements UserDetails {
    
        private Integer id; // 用户id
        private String username;    // 用户名
        private String password;    // 密码
        private boolean enabled;    // 是否可用
        private boolean accountNonExpired;  // 账户过期
        private boolean accountNonLocked;   // 账户锁定
        private boolean credentialsNonExpired;  // 凭证过期
        private List<Role> roles = new ArrayList<>();   // 用户角色信息
        
        // set和get方法以及重写的方法...(前面有提到)
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • Role
    package com.vinjcent.pojo;
    
    import java.io.Serializable;
    
    public class Role implements Serializable {
    
        private Integer id;
        private String name;
        private String nameZh;
    
        // set和get方法以及重写的方法...(前面有提到)
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 编写mapper、service、xml文件(这里只写接口,看接口实现方法)
    • UserMapper
    package com.vinjcent.mapper;
    
    import com.vinjcent.pojo.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.springframework.stereotype.Repository;
    
    /**
     * @author vinjcent
     * @description 针对表【user】的数据库操作Mapper
     * @createDate 2022-09-25 12:03:42
     */
    @Mapper
    @Repository
    public interface UserMapper {
    
        // 根据用户名返回用户信息
        User queryUserByUsername(@Param("username") String username);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • RoleMapper
    package com.vinjcent.mapper;
    
    
    import com.vinjcent.pojo.Role;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    /**
     * @author vinjcent
     * @description 针对表【role】的数据库操作Mapper
     * @createDate 2022-09-25 12:01:18
     */
    @Mapper
    @Repository
    public interface RoleMapper {
    
        List<Role> queryRolesByUid(@Param("uid") Integer uid);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    1. 自定义 DivUserDetailsService 实现 UserDetailsService,作为数据源进行身份认证
    • UserDetailsService
    package com.vinjcent.config.security;
    
    import com.vinjcent.pojo.Role;
    import com.vinjcent.pojo.User;
    import com.vinjcent.service.RoleService;
    import com.vinjcent.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    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.Component;
    import org.springframework.util.ObjectUtils;
    
    import java.util.List;
    
    @Component
    public class DivUserDetailsService implements UserDetailsService {
    
        // dao ===> springboot + mybatis
        private final UserService userService;
    
        private final RoleService roleService;
    
        @Autowired
        public DivUserDetailsService(UserService userService, RoleService roleService) {
            this.userService = userService;
            this.roleService = roleService;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 1.查询用户
            User user = userService.queryUserByUsername(username);
            if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
            // 2.查询权限信息
            List<Role> roles = roleService.queryRolesByUid(user.getId());
            user.setRoles(roles);
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    1. 配置类 WebMvcConfigurer、WebSecurityConfigurerAdapter
    • WebMvcConfigurer
    package com.vinjcent.config.web;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class DivWebMvcConfigurer implements WebMvcConfigurer {
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            // 当访问url路径,映射一个view视图
            registry.addViewController("/toLogin").setViewName("login");
            registry.addViewController("/index").setViewName("index");
            registry.addViewController("/toIndex").setViewName("index");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • WebSecurityConfigurerAdapter
    package com.vinjcent.config.security;
    
    
    import org.springframework.beans.factory.annotation.Autowired;
    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.WebSecurityConfigurerAdapter;
    
    import javax.annotation.Resource;
    
    
    /**
     *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
     */
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 构造注入使用@Autowired,set注入使用@Resource
        private final DivUserDetailsService userDetailsService;
    
        // UserDetailsService
        @Autowired
        public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    
        // AuthenticationManager
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        // 拦配置http拦截
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .mvcMatchers("/toLogin").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/toLogin")
                    .loginProcessingUrl("/login")
                    .usernameParameter("uname")
                    .passwordParameter("passwd")
                    .defaultSuccessUrl("/toIndex", true)    // 默认成功重定向
                    .failureUrl("/toLogin")     // 失败登录重定向
                    .and()
                    .logout()
                    .logoutUrl("/logout")		// 登出url
                    .logoutSuccessUrl("/toLogin")	// 登出成功之后转发url
                    .and()
                    .csrf()
                    .disable();
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    1. html页面视图
    • index.html
    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <head>
        <meta charset="UTF-8">
        <title>系统主页title>
    head>
    <body>
    <h1>欢迎<span sec:authentication="principal.username">span>,进入我的主页!h1>
    
    <hr>
    <h1>获取认证用户信息h1>
    <ul>
        <li sec:authentication="principal.username">li>
        <li sec:authentication="principal.authorities">li>
        <li sec:authentication="principal.accountNonExpired">li>
        <li sec:authentication="principal.accountNonLocked">li>
        <li sec:authentication="principal.credentialsNonExpired">li>
    ul>
    
    
    <a th:href="@{/logout}">退出登录a>
    
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • login.html
    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录页面title>
    head>
    <body>
    
        <h1>用户登录h1>
        <form th:action="@{/login}" method="post">
            用户名: <input type="text" name="uname"> <br>
            密码: <input type="password" name="passwd"> <br>
            <input type="submit" value="登录">
        form>
    <h3>
        <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}">div>
    h3>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    1. 根据数据库数据进行测试

    在这里插入图片描述

    6.2 前后端分离认证总结案例

    根据前面章节的分析,发现在 Security 进行认证的时候,走的是 UsernamePasswordAuthenticationFilter 过滤器,并且调用的方法是 attemptAuthentication() 方法,并返回 Authentication 对象。传统 web 的认证方式并不满足前后端分离使用 json 数据格式进行交互,我们需要对认证用户信息的这个过滤器进行重写

    在这里插入图片描述

    • 对于 http 请求过滤器的配置

    在这里插入图片描述

    1. 创建一个模块 spring-security-04-separate

    2. 导入依赖pom.xml

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.2.2version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.22version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.2.8version>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    1. application.yml配置文件
    # 端口号
    server:
      port: 3033
    # 服务应用名称
    spring:
      application:
        name: SpringSecurity04-separate
      # 数据源
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
    mybatis:
      # 注意 mapper 映射文件必须使用"/"
      type-aliases-package: com.vinjcent.pojo
      mapper-locations: com/vinjcent/mapper/**/*.xml
    
    # 日志处理,为了展示 mybatis 运行 sql 语句
    logging:
      level:
        com:
          vinjcent:
            debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    1. 编写实体类User、Role
    • User
    package com.vinjcent.pojo;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.*;
    
    // 自定义用户User
    public class User implements UserDetails {
    
        private Integer id; // 用户id
        private String username;    // 用户名
        private String password;    // 密码
        private boolean enabled;    // 是否可用
        private boolean accountNonExpired;  // 账户过期
        private boolean accountNonLocked;   // 账户锁定
        private boolean credentialsNonExpired;  // 凭证过期
        private List<Role> roles = new ArrayList<>();   // 用户角色信息
        
        // set和get方法以及重写的方法...(前面有提到)
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • Role
    package com.vinjcent.pojo;
    
    import java.io.Serializable;
    
    public class Role implements Serializable {
    
        private Integer id;
        private String name;
        private String nameZh;
    
        // set和get方法以及重写的方法...(前面有提到)
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 编写mapper、service、xml文件(这里只写接口,看接口实现方法)
    • UserMapper
    package com.vinjcent.mapper;
    
    import com.vinjcent.pojo.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.springframework.stereotype.Repository;
    
    /**
     * @author vinjcent
     * @description 针对表【user】的数据库操作Mapper
     * @createDate 2022-09-25 12:03:42
     */
    @Mapper
    @Repository
    public interface UserMapper {
    
        // 根据用户名返回用户信息
        User queryUserByUsername(@Param("username") String username);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • RoleMapper
    package com.vinjcent.mapper;
    
    
    import com.vinjcent.pojo.Role;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    /**
     * @author vinjcent
     * @description 针对表【role】的数据库操作Mapper
     * @createDate 2022-09-25 12:01:18
     */
    @Mapper
    @Repository
    public interface RoleMapper {
    
        List<Role> queryRolesByUid(@Param("uid") Integer uid);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    1. 自定义 UserDetailsService 实现 UserDetailsService,作为数据源认证身份
    • UserDetailsService
    package com.vinjcent.config.security;
    
    import com.vinjcent.pojo.Role;
    import com.vinjcent.pojo.User;
    import com.vinjcent.service.RoleService;
    import com.vinjcent.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    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.Component;
    import org.springframework.util.ObjectUtils;
    
    import java.util.List;
    
    @Component
    public class DivUserDetailsService implements UserDetailsService {
    
        // dao ===> springboot + mybatis
        private final UserService userService;
    
        private final RoleService roleService;
    
        @Autowired
        public DivUserDetailsService(UserService userService, RoleService roleService) {
            this.userService = userService;
            this.roleService = roleService;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 1.查询用户
            User user = userService.queryUserByUsername(username);
            if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
            // 2.查询权限信息
            List<Role> roles = roleService.queryRolesByUid(user.getId());
            user.setRoles(roles);
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    1. 编写 LoginFilter 继承 UsernamePasswordAuthenticationFilter 过滤器类
    package com.vinjcent.filter;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Map;
    
    /**
     * 自定义前后端分离的 Filter,重写 UsernamePasswordAuthenticationFilter
     */
    public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    
        // 用于指定请求类型
        private boolean postOnly = true;
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            // 1.判断是否满足 POST 类型的请求
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            // 2.判断使用的数据格式类型是否是json
            if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
                // 如果是json格式,需要转化成对象并从中获取用户输入的用户名和密码进行认证 {"username": "root","password": "123"}
                try {
                    Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                    // 将用户名(username)和密码(password)通过动态传递的方式,进行获取
                    // getUsernameParameter()、getPasswordParameter()是父类的方法,通过父类设置这两个属性的值
                    String username = userInfo.get(getUsernameParameter());
                    String password = userInfo.get(getPasswordParameter());
                    System.out.println("用户名: " + username + " 密码: " + password);
                    // 生成用户令牌
                    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
                    setDetails(request, token);
                    // 为了保证自定义的过滤器拥有 AuthenticationManager,我们还需手动配置一个
                    return this.getAuthenticationManager().authenticate(token);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            return super.attemptAuthentication(request, response);
        }
    
        @Override
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    1. 自定义认证成功、认证失败、退出登录处理事件
    • 认证成功
    package com.vinjcent.handler;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 自定义认证成功之后处理
     */
    public class DivAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            Map<String, Object> result = new HashMap<>();
            result.put("msg","登陆成功");
            result.put("status", 200);
            result.put("用户信息", authentication.getPrincipal());
            response.setContentType("application/json;charset=UTF-8");
            // 响应返回状态
            response.setStatus(HttpStatus.OK.value());
            String info = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(info);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 认证失败
    package com.vinjcent.handler;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 自定义认证失败之后处理
     */
    public class DivAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            Map<String, Object> result = new HashMap<>();
            result.put("msg", "登陆失败: " + exception.getMessage());
            result.put("status", 500);
            response.setContentType("application/json;charset=UTF-8");
            // 响应返回状态
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            String info = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(info);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 退出登录
    package com.vinjcent.handler;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 自定义注销成功之后处理
     */
    public class DivLogoutSuccessHandler implements LogoutSuccessHandler {
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            Map<String, Object> result = new HashMap<>();
            result.put("msg","注销成功,当前认证对象为:" + authentication);
            result.put("status", 200);
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpStatus.OK.value());
            String info = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(info);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    1. 编写配置类 WebSecurityConfiguration 继承 WebSecurityConfigurerAdapter
    • WebSecurityConfiguration
    package com.vinjcent.config.security;
    
    import com.vinjcent.filter.LoginFilter;
    import com.vinjcent.handler.DivAuthenticationFailureHandler;
    import com.vinjcent.handler.DivAuthenticationSuccessHandler;
    import com.vinjcent.handler.DivLogoutSuccessHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.AuthenticationManager;
    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.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 注入数据源认证
        private final DivUserDetailsService userDetailsService;
    
        @Autowired
        public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    
    
        // 自定义AuthenticationManager(自定义需要暴露该bean)
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        // 暴露AuthenticationManager,使得这个bean能在组件中进行注入
        @Override
        @Bean
        public AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
        @Bean
        public LoginFilter loginFilter() throws Exception {
            // 1.创建自定义的LoginFilter对象
            LoginFilter loginFilter = new LoginFilter();
            // 2.设置登陆操作的请求
            loginFilter.setFilterProcessesUrl("/login");
            // 3.动态设置传递的参数key
            loginFilter.setUsernameParameter("uname");  // 指定 json 中的用户名key
            loginFilter.setPasswordParameter("passwd"); // 指定 json 中的密码key
            // 4.设置自定义的用户认证管理者
            loginFilter.setAuthenticationManager(authenticationManager());
            // 5.配置认证成功/失败处理(前后端分离)
            loginFilter.setAuthenticationSuccessHandler(new DivAuthenticationSuccessHandler());  // 认证成功处理
            loginFilter.setAuthenticationFailureHandler(new DivAuthenticationFailureHandler());  // 认证失败处理
            return loginFilter;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .anyRequest().authenticated()  // 所有请求必须认证
                    .and()
                    .formLogin()    // 登录处理
                    .and()
                    .logout()
                    .logoutUrl("/logout")   // 登出处理(也可以通过自定logoutRequestMatcher配置登出请求url和请求类型)
                    .logoutSuccessHandler(new DivLogoutSuccessHandler())    // 注销登录成功处理
                    .and()
                    .exceptionHandling()    // 异常处理(用于未认证处理返回的数据)
                    .authenticationEntryPoint(((req, resp, ex) -> {
                        // 设置响应内容类型"application/json;charset=UTF-8"
                        resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                        // 设置响应状态码为"未授权"401
                        resp.setStatus(HttpStatus.UNAUTHORIZED.value());
                        // 返回响应数据
                        resp.getWriter().println("请认证之后再操作!");
                    })) // 配置认证入口点异常处理
                    .and()
                    .csrf()
                    .disable();     // 关闭csrf跨域请求
    
            // 替换原始 UsernamePasswordAuthenticationFilter 过滤器
            http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
            /**
                http.addFilter();   // 添加一个过滤器
                http.addFilterAt(); // at: 添加一个过滤器,将过滤链中的某个过滤器进行替换
                http.addFilterBefore(); // before: 添加一个过滤器,追加到某个具体过滤器之前
                http.addFilterAfter();  // after: 添加一个过滤器,追加到某个具体过滤器之后
             */
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    1. 配置一个测试接口,作为请求资源进行测试
    package com.vinjcent.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class TestController {
        
        @RequestMapping("/test")
        public String test() {
            return "Hello SpringSecurity!";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 未认证时请求资源

    在这里插入图片描述

    • 进行登录失败操作

    在这里插入图片描述

    • 进行登录成功操作

    在这里插入图片描述

    • 退出登录操作

    在这里插入图片描述

    6.3 传统 web 开发之添加验证码

    在 6.1 传统web开发认证总结案例开发基础上进行修改

    1. 添加依赖pom.xml
    
    <dependency>
        <groupId>com.github.pengglegroupId>
        <artifactId>kaptchaartifactId>
        <version>2.3.2version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 添加验证码配置类 KaptchaConfiguration
    • KaptchaConfiguration
    package com.vinjcent.config.verification;
    
    import com.google.code.kaptcha.Producer;
    import com.google.code.kaptcha.impl.DefaultKaptcha;
    import com.google.code.kaptcha.util.Config;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.Properties;
    
    /**
     * 自定义认证码,源自google
     */
    @Configuration
    public class KaptchaConfiguration {
    
        @Bean
        public Producer kaptcha() {
            Properties properties = new Properties();
            // 1.验证码宽度
            properties.setProperty("kaptcha.image.width", "150");
            // 2.验证码高度
            properties.setProperty("kaptcha.image.height", "50");
            // 3.验证码字符串
            properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
            // 4.验证码长度
            properties.setProperty("kaptcha.textproducer.char.length", "4");
            Config config = new Config(properties);
            DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
            defaultKaptcha.setConfig(config);
            return defaultKaptcha;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    1. 返回图片处理结果并存储在 session 中,编写 controller
    • VerifyCodeController
    package com.vinjcent.controller;
    
    import com.google.code.kaptcha.Producer;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import javax.imageio.ImageIO;
    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    
    @Controller
    public class VerifyCodeController {
    
        private final Producer producer;
    
        @Autowired
        public VerifyCodeController(Producer producer) {
            this.producer = producer;
        }
    
        @RequestMapping("/vc.jpg")
        public void verifyCode(HttpServletResponse response, HttpSession session) throws IOException {
            // 1.生成验证码
            String verifyCode = producer.createText();
            // 2.保存到 session(可以存入到redis当中)
            session.setAttribute("kaptcha", verifyCode);
            // 3.生成图片
            BufferedImage image = producer.createImage(verifyCode);
            // 4.设置响应类型
            response.setContentType("image/png#pic_center =800x");
            // 5.响应图片
            ServletOutputStream os = response.getOutputStream();
            ImageIO.write(image, "jpg", os);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    1. 自定义登录验和证码过滤器
    • LoginKaptchaFilter
    package com.vinjcent.filter;
    
    import com.vinjcent.exception.KaptchaNotMatchException;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.util.ObjectUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * 自定义登录验证码 filter
     */
    public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
    
        private boolean postOnly = true;
    
        public static final String SPRING_SECURITY_FORM_KAPTCHA = "kaptcha";
    
        private String kaptchaParameter = SPRING_SECURITY_FORM_KAPTCHA;
    
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            // 1.先判断是否为 POST 请求
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            // 2.从请求中获取验证码
            String verifyCode = request.getParameter(getKaptchaParameter());
            // 3.获取 session 中验证码进行比较
            String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
            // 4.与 session 中验证码进行比较
            if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equals(sessionVerifyCode)) {
                return super.attemptAuthentication(request, response);
            }
            // 5.如果匹配不上,抛出自定义异常
            throw new KaptchaNotMatchException("验证码不匹配!");
        }
    
    
        @Override
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    
        public String getKaptchaParameter() {
            return kaptchaParameter;
        }
    
        public void setKaptchaParameter(String kaptchaParameter) {
            this.kaptchaParameter = kaptchaParameter;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    1. 自定义验证码不匹配异常
    • KaptchaNotMatchException
    package com.vinjcent.exception;
    
    
    import org.springframework.security.core.AuthenticationException;
    
    /**
     * 自定义验证码异常 exception
     */
    public class KaptchaNotMatchException extends AuthenticationException {
    
    
        public KaptchaNotMatchException(String msg, Throwable cause) {
            super(msg, cause);
        }
    
        public KaptchaNotMatchException(String msg) {
            super(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    1. 修改 WebSecurityConfiguration 配置类,将自定义的过滤器进行替换
    • WebSecurityConfiguration
    package com.vinjcent.config.security;
    
    
    import com.vinjcent.filter.LoginKaptchaFilter;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    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.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    
    /**
     *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
     */
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 构造注入使用@Autowired,set注入使用@Resource
        private final DivUserDetailsService userDetailsService;
    
        @Autowired
        public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    
        // 自定义AuthenticationManager(自定义需要暴露该bean)
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        // 暴露AuthenticationManager,使得这个bean能在组件中进行注入
        @Override
        @Bean
        public AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
        // 自定义认证过滤器
        @Bean
        public LoginKaptchaFilter loginKaptchaFilter() throws Exception {
            LoginKaptchaFilter loginKaptchaFilter = new LoginKaptchaFilter();
            // 动态绑定参数
            loginKaptchaFilter.setUsernameParameter("uname");
            loginKaptchaFilter.setPasswordParameter("passwd");
            loginKaptchaFilter.setKaptchaParameter("kaptcha");
            // 指定认证管理器
            loginKaptchaFilter.setAuthenticationManager(authenticationManager());
            // 指定认证url
            loginKaptchaFilter.setFilterProcessesUrl("/login");
            // 指定认证成功处理
            loginKaptchaFilter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
                response.sendRedirect("/index");
            }));
            // 指定认证失败处理
            loginKaptchaFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
                response.sendRedirect("/toLogin");
            }));
            return loginKaptchaFilter;
        }
    
        // 拦配置http拦截
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .mvcMatchers("/toLogin").permitAll()
                    .mvcMatchers("/vc.jpg").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/toLogin")
                    .and()
                    .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/toLogin")
                    .and()
                    .csrf()
                    .disable();
            // 替换自定义过滤器
            http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    1. 运行测试

    6.4 前后端分离开发之添加验证码

    在 6.2 传统web开发认证总结案例开发基础上进行修改

    1. 添加依赖pom.xml
    
    <dependency>
        <groupId>com.github.pengglegroupId>
        <artifactId>kaptchaartifactId>
        <version>2.3.2version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 添加验证码配置类 KaptchaConfiguration
    • KaptchaConfiguration
    package com.vinjcent.config.verification;
    
    import com.google.code.kaptcha.Producer;
    import com.google.code.kaptcha.impl.DefaultKaptcha;
    import com.google.code.kaptcha.util.Config;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.Properties;
    
    /**
     * 自定义认证码,源自google
     */
    @Configuration
    public class KaptchaConfiguration {
    
        @Bean
        public Producer kaptcha() {
            Properties properties = new Properties();
            // 1.验证码宽度
            properties.setProperty("kaptcha.image.width", "150");
            // 2.验证码高度
            properties.setProperty("kaptcha.image.height", "50");
            // 3.验证码字符串
            properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
            // 4.验证码长度
            properties.setProperty("kaptcha.textproducer.char.length", "4");
            Config config = new Config(properties);
            DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
            defaultKaptcha.setConfig(config);
            return defaultKaptcha;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    1. 返回图片处理结果并存储在 session 中,编写 controller(这里跟传统 web 开发有点不同,需要将图片转为Base64编码格式)
    package com.vinjcent.controller;
    
    import com.google.code.kaptcha.Producer;
    import org.apache.tomcat.util.codec.binary.Base64;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.util.FastByteArrayOutputStream;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.imageio.ImageIO;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    
    @RestController
    public class VerifyCodeController {
    
        private final Producer producer;
    
        @Autowired
        public VerifyCodeController(Producer producer) {
            this.producer = producer;
        }
    
        @RequestMapping("/vc.jpg")
        public String verifyCode(HttpSession session) throws IOException {
            // 1.生成验证码
            String verifyCode = producer.createText();
            // 2.保存到 session(可以存入到redis当中)
            session.setAttribute("kaptcha", verifyCode);
            // 3.生成图片
            BufferedImage image = producer.createImage(verifyCode);
            FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
            ImageIO.write(image, "jpg", fos);
    
            // 4.将生成的图片转为Base64格式返回给前端
            return Base64.encodeBase64String(fos.toByteArray());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    1. 自定义登录验和证码过滤器
    • LoginKaptchaFilter
    package com.vinjcent.filter;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.vinjcent.exception.KaptchaNotMatchException;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.util.ObjectUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Map;
    
    /**
     * 自定义登录验证码 filter
     */
    public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
    
        private boolean postOnly = true;
    
        public static final String SPRING_SECURITY_FORM_KAPTCHA = "kaptcha";
    
        private String kaptchaParameter = SPRING_SECURITY_FORM_KAPTCHA;
    
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            // 1.先判断是否为 POST 请求
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            try {
                // 2.通过key-value形式读取流中的文件
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                // 获取用户名
                String username = userInfo.get(getUsernameParameter());
                // 获取密码
                String password = userInfo.get(getPasswordParameter());
                // 获取验证码
                String verifyCode = userInfo.get(getKaptchaParameter());
                // 3.获取 session 中的验证码
                String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
                // 4.将当前用户输入的验证码与 session 中的验证码进行比较
                if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equals(sessionVerifyCode)) {
                    // 封装username&password的token
                    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                    // Allow subclasses to set the "details" property
                    setDetails(request, authRequest);
                    return this.getAuthenticationManager().authenticate(authRequest);
                }
                // 5.如果匹配不上,抛出自定义异常
                throw new KaptchaNotMatchException("验证码不匹配!");
    
            } catch (IOException e) {
                e.printStackTrace();
            }
    
    
            // 2.从请求中获取验证码
            String verifyCode = request.getParameter(getKaptchaParameter());
            // 3.获取 session 中验证码进行比较
            String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
            // 4.与 session 中验证码进行比较
            if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equals(sessionVerifyCode)) {
                return super.attemptAuthentication(request, response);
            }
            // 5.如果匹配不上,抛出自定义异常
            throw new KaptchaNotMatchException("验证码不匹配!");
        }
    
    
        @Override
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    
        public String getKaptchaParameter() {
            return kaptchaParameter;
        }
    
        public void setKaptchaParameter(String kaptchaParameter) {
            this.kaptchaParameter = kaptchaParameter;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    1. 自定义验证码不匹配异常
    • KaptchaNotMatchException
    package com.vinjcent.exception;
    
    
    import org.springframework.security.core.AuthenticationException;
    
    /**
     * 自定义验证码异常 exception
     */
    public class KaptchaNotMatchException extends AuthenticationException {
    
    
        public KaptchaNotMatchException(String msg, Throwable cause) {
            super(msg, cause);
        }
    
        public KaptchaNotMatchException(String msg) {
            super(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    1. 修改 WebSecurityConfiguration 配置类,将自定义的过滤器进行替换
    • WebSecurityConfiguration
    package com.vinjcent.config.security;
    
    import com.vinjcent.filter.LoginKaptchaFilter;
    import com.vinjcent.handler.DivAuthenticationFailureHandler;
    import com.vinjcent.handler.DivAuthenticationSuccessHandler;
    import com.vinjcent.handler.DivLogoutSuccessHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.AuthenticationManager;
    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.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 注入数据源认证
        private final DivUserDetailsService userDetailsService;
    
        @Autowired
        public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    
    
        // 自定义AuthenticationManager(自定义需要暴露该bean)
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        // 暴露AuthenticationManager,使得这个bean能在组件中进行注入
        @Override
        @Bean
        public AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
        @Bean
        public LoginKaptchaFilter loginKaptchaFilter() throws Exception {
            // 1.创建自定义的LoginFilter对象
            LoginKaptchaFilter loginFilter = new LoginKaptchaFilter();
            // 2.设置登陆操作的请求
            loginFilter.setFilterProcessesUrl("/login");
            // 3.动态设置传递的参数key
            loginFilter.setUsernameParameter("uname");  // 指定 json 中的用户名key
            loginFilter.setPasswordParameter("passwd"); // 指定 json 中的密码key
            loginFilter.setKaptchaParameter("kaptcha"); // 指定 json 中的验证码
            // 4.设置自定义的用户认证管理者
            loginFilter.setAuthenticationManager(authenticationManager());
            // 5.配置认证成功/失败处理(前后端分离)
            loginFilter.setAuthenticationSuccessHandler(new DivAuthenticationSuccessHandler());  // 认证成功处理
            loginFilter.setAuthenticationFailureHandler(new DivAuthenticationFailureHandler());  // 认证失败处理
            return loginFilter;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .mvcMatchers("/vc.jpg").permitAll()
                    .anyRequest().authenticated()  // 所有请求必须认证
                    .and()
                    .formLogin()    // 登录处理
                    .and()
                    .logout()
                    .logoutUrl("/logout")   // 登出处理(也可以通过自定logoutRequestMatcher配置登出请求url和请求类型)
                    .logoutSuccessHandler(new DivLogoutSuccessHandler())    // 注销登录成功处理
                    .and()
                    .exceptionHandling()    // 异常处理(用于未认证处理返回的数据)
                    .authenticationEntryPoint(((req, resp, ex) -> {
                        // 设置响应内容类型"application/json;charset=UTF-8"
                        resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                        // 设置响应状态码为"未授权"401
                        resp.setStatus(HttpStatus.UNAUTHORIZED.value());
                        resp.getWriter().println("请认证之后再操作!");
                    })) // 配置认证入口点异常处理
                    .and()
                    .csrf()
                    .disable();     // 关闭csrf跨域请求
    
            // 替换原始 UsernamePasswordAuthenticationFilter 过滤器
            http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
            /**
                http.addFilter();   // 添加一个过滤器
                http.addFilterAt(); // at: 添加一个过滤器,将过滤链中的某个过滤器进行替换
                http.addFilterBefore(); // before: 添加一个过滤器,追加到某个具体过滤器之前
                http.addFilterAfter();  // after: 添加一个过滤器,追加到某个具体过滤器之后
             */
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    1. 运行测试,使用postman测试工具进行测试

    1)先进行请求/vc.jpg生成图片,并将图片信息保存到 session 中

    前端由Base64转为图片时,需要添加前缀:data:image/png;base64,

    在这里插入图片描述

    2)可以使用在线工具,解析Base64编码

    在这里插入图片描述

    3)解析之后再请求登录操作

    在这里插入图片描述

  • 相关阅读:
    deckGL自定义图层学习笔记
    ROC 曲线:健康背景下的应用和解释
    水利设计公司资质怎么办理,办理水利设计有限公司及水利丙级资质申请程序
    两数相加 js
    mybatis参数为0识别为空字符串的查询处理
    计及电池储能寿命损耗的微电网经济调度(matlab代码)
    Git新技能-stash操作
    Qt学习20 Qt 中的标准对话框(中)
    攻防世界_MISC之碎纸机11
    中国石油大学(北京)-《 油气田开发方案设计》第二阶段在线作业
  • 原文地址:https://blog.csdn.net/Wei_Naijia/article/details/127650173