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

六、自定义认证案例


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

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

  2. 导入依赖pom.xml

<dependencies>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--security-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!--thymeleaf-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!--thymeleaf-security-->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.22</version>
    </dependency>
    <!--druid-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.8</version>
    </dependency>
</dependencies>
  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. 编写实体类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方法以及重写的方法...(前面有提到)
	
}
  • 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. 编写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);

}
  • 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. 自定义 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. 配置类 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");
    }
}
  • 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. 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>
  • 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. 根据数据库数据进行测试

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

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

  2. 导入依赖pom.xml

<dependencies>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--security-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.22</version>
    </dependency>
    <!--druid-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.8</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>
  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. 编写实体类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方法以及重写的方法...(前面有提到)
	
}
  • 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. 编写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);

}
  • 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. 自定义 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. 编写 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. 自定义认证成功、认证失败、退出登录处理事件
  • 认证成功
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);
    }
}
  • 认证失败
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);
    }
}
  • 退出登录
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. 编写配置类 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. 配置一个测试接口,作为请求资源进行测试
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!";
    }
}
  • 未认证时请求资源

在这里插入图片描述

  • 进行登录失败操作

在这里插入图片描述

  • 进行登录成功操作

在这里插入图片描述

  • 退出登录操作

在这里插入图片描述

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

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

  1. 添加依赖pom.xml
<!--verification-->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
  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. 返回图片处理结果并存储在 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. 自定义登录验和证码过滤器
  • 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. 自定义验证码不匹配异常
  • 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. 修改 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. 运行测试

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

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

  1. 添加依赖pom.xml
<!--verification-->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
  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. 返回图片处理结果并存储在 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. 自定义登录验和证码过滤器
  • 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. 自定义验证码不匹配异常
  • 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. 修改 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. 运行测试,使用postman测试工具进行测试

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Naijia_OvO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值