跳至主要內容

SpringSecurity开发实战

wangdx大约 29 分钟

SpringSecurity 简介

WEB 资源访问

  • 在现代的开发中,WEB 应用已经成为了非常广泛的技术展现形式,使用 WEB 开发不仅可以轻松的实现程序代码的维护,同时用户也可以通过浏览器方便的进行应用的访问由于 WEB 应用都是基于 HTTP 公共协议构建出来的,用户只需要通过正确的访问地址,就可以轻松的访问到 WEB 应用中的各类资源

核心资源保护

  • 随着互联网在世界范围内的不断发展,WEB 应用程序也越来越普及,而伴随着不同的业务平台的需要,不同的 WEB 应用中都会保存有大量重要的资源数据,所以为了防止资源泄露,就需要对资源进行有效的保护

用户认证与授权处理

  • 如果要想实现核心资源的保护首先需要做的处理就是限制资源的对外访问,强制要求所有的使用者按照应用的要求进行用户认证处理,这样 WEB 服务就可以根据用户登最终依据这些权限数据实现用户授权的检查,由于不同录的信息获取到相应权限数据,的用户有不同的权限,所以这类信息需要保存在 session 属性范围之中

SpringSecurity

  • SpringSecurity(前身是“Acegi Security”开发框架)是一套完整的 WEB 安全性应用解决方案,其主要基于 SpringAOP 与过滤器实现安全访问控制,在 SpringSecurity 之中有两大核心主题:
    • 用户认证(Authentication):主要依靠用户名和密码进行处理,并判断当前用户的状态是否为合法状态;
    • 用户授权(Authorization):每一个用户在系统中都会存在有不同的角色或者是不同的权限,利用角色或权限的判断可以对资源进行有效的分类管理,以保证每个用户操作资源的安全性。

SpringSecurity 控制流程

  • 在 SpringSecurity 之中为了实现安全管理提供有一系列的访问过滤器,而所有的访问过滤器都只围绕着两个主题展开:认证管理、决策管理。所有的认管理都会由 SpringSecurity 负责,开发者只需要按照其既定的结构要求进行相关代码配置,即可实现资源安全访问控制。

SpringSecurity 快速启动

SpringSecurity 开发整合

  • 为了便于开发者使用 SpringSecurity 进行项目的开发由 Spring 开发框架提供了其开发的专属依赖库,该依赖库包含了安全框架的核心配置、页面标签、WEB 整合等功能,开发者只需要引入这些依赖并配置后 SpringSecurity 即可根据开发者的配置,自动实现认证与授权处理
1、

implementation group: 'org.springframework.security', name: 'spring-security-core', version: '6.0.0-M3'
implementation group: 'org.springframework.security', name: 'spring-security-web', version: '6.0.0-M3'
implementation group: 'org.springframework.security', name: 'spring-security-config', version: '6.0.0-M3'
implementation group: 'org.springframework.security', name: 'spring-security-taglibs', version: '6.0.0-M3'





2、
    'spring-security-core' : "org.springframework.security:spring-security-core:${versions.spring}",
    'spring-security-web' : "org.springframework.security:spring-security-web:${versions.spring}",
    'spring-security-config' : "org.springframework.security:spring-security-config:${versions.spring}",
    'spring-security-taglibs' : "org.springframework.security:spring-security-taglibs:${versions.spring}"

3、
    @Override
    protected Filter[] getServletFilters() { // 配置过滤器
        CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
        encodingFilter.setEncoding("UTF-8"); // 编码设置
        encodingFilter.setForceEncoding(true); // 强制编码
        // 定义SpringSedcurity过滤链的处理类,这个配置直接表示引入了SpringSecurity
        DelegatingFilterProxy delegatingFilterProxy =
                new DelegatingFilterProxy("springSecurityFilterChain");
        return new Filter[] {encodingFilter, delegatingFilterProxy};
    }
4、
package com.yootk.config;

import com.sun.net.httpserver.HttpServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // 启用SpringSecurity支持
public class WebMVCSecurityConfiguration { // WEB配置类
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**").authenticated() // 配置认证目录
                .antMatchers("/**").permitAll(); // 任意访问
        http.formLogin(); // SpringSecurity内部自带登录表单
        return http.build();
    }
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web -> web.ignoring().antMatchers("/yootk-images/**", "yootk-js/**", "/yootk-css/**"));
    }
}


5、
package com.yootk.action;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/pages/message/")
public class MessageAction {
    @GetMapping("info")
    @ResponseBody // 直接REST结果响应
    public Object info() {
        Map<String, String> result = new HashMap<>();
        result.put("yootk", "沐言科技");
        result.put("lee", "可爱的小李老师");
        return result;
    }
}


6、
localhost/pages/message/info

7、
localhost/login

UserDetailsService

UserDetaisService

  • 用户登录逻辑的核心关键在于用户数据的获取,即:用户输入用户名和密码之后重要根据用户名获取数据项,而后再通过密码进行认证结果的判断。所以为了解决用户数据获取操作的问题,SpringSecurity 提供了 UserDetailsService 数据加载接口,该接口的关联结构如图所示。开发者只需要通过该接口提供的 loadUserByUsername()业务方法返回一个 UserDetails 接口实例,随后将此数据对象实例设置在 SpringSecurity 的使用环境之中,以后就可以由 SpringSecurity 自动进行认证处理逻辑的实现。

1、
package com.yootk.test;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class CreateSecurityPassword { // 创建加密密码
    public static void main(String[] args) {
        String password = "hello"; // 明文为HELLO
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); // 定义加密器
        String cipherText = encoder.encode(password); // 数据的加密操作
        System.out.println("加密后的密码:" + cipherText);
        System.out.println("密码比较:" + encoder.matches(password, cipherText));
    }
}


2、
package com.yootk.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class YootkUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 本次的开发操作将使用默认的用户名”yootk“(如果要是更换为数据库,此处可以通过数据库加载)
        // SpringSecurity之中使用GrantedAuthority接口来描述所有用户具备的权限
        List<GrantedAuthority> authorityList = new ArrayList<>();
        if ("yootk".equals(username)) { // 为固定管理用户授权
            // 在进行授权配置的时候,SpringSecurity权限必须使用”ROLE_“前缀配置
            authorityList.add(new SimpleGrantedAuthority("ROLE_NEWS")); // 授权项
            authorityList.add(new SimpleGrantedAuthority("ROLE_ADMIN")); // 授权项
            authorityList.add(new SimpleGrantedAuthority("ROLE_SYSTE")); // 授权项
        }
        // 所有用户都具有的一个公共权限,用于访问”/pages/message/**“下的资源
        authorityList.add(new SimpleGrantedAuthority("ROLE_MESSAGE")); // 授权项
        // 此时用户可以输入任意的用户名来实现登录认证(伪认证的逻辑)
        User user = new User(username,
                "$2a$10$3FF/v9EY11s7aTRsS24C.eNITPiWjJiAMjXGVJpcwkTAba.6YrYnW",
                authorityList); // 获取用户信息
        return user;
    }
}


3、
localhost/pages/message/info

4、
    @Bean
    public PasswordEncoder passwordEncoder() { // 定义密码加密器
        return new BCryptPasswordEncoder();
    }

认证与授权表达式

1、
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                        .access("isAuthenticated()"); // 只允许认证后的用户访问
        http.formLogin(); // SpringSecurity内部自带登录表单
        return http.build();
    }

2、
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                        .access("isAuthenticated()") // 只允许认证后的用户访问
                .antMatchers("/pages/message/**")
                        .access("hasRole('ADMIN')"); // 拥有ROLE_ADMIN角色
        http.formLogin(); // SpringSecurity内部自带登录表单
        return http.build();
    }

3、
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                        .access("isAuthenticated()") // 只允许认证后的用户访问
                .antMatchers("/pages/message/**")
                        .access("hasAnyRole('ADMIN', 'NEWS', 'MESSAGE')"); // 任意一个角色都可以访问
        http.formLogin(); // SpringSecurity内部自带登录表单
        return http.build();
    }

4、
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                .access("isAuthenticated()") // 只允许认证后的用户访问
                .antMatchers("/pages/message/**")
                .access("hasAnyRole('ADMIN', 'NEWS', 'MESSAGE')") // 任意一个角色都可以访问
                .antMatchers("/pages/news/**") // 路径不存在
                .access("hasRole('NEWS')")
                .antMatchers("/**").access("permitAll");
        http.formLogin(); // SpringSecurity内部自带登录表单
        return http.build();
    }

SecurityContextHolder

SpringSecurity 获取认证用户信息

  • 在用户登录完成之后,所有用户的信息都会被 SpringSecurity 所管理,但是在实际的项经常需要通过当前的用户名进行相关的业务处理操作,所以为了满足这样目开发之中,SpringSecurity 提供了 SecurityContextHolder 处理类的开发需求,,该类可以获取 SecurityContext 接口实例,这样就可以获取到当前用户的认证信息(Authentication)

用户认证数据

  • 在 SpringSecurity 之中所有用户认证数据统一通过 Authentication 接口实例进行保存需要注意的是,不管当前用户是否认证,SecurityContext 都会返回一个与当前 Session 匹配的 Authentication 接口实例,而只有在认证成功之后,才会在此接口实例中保存相应的数据
1、
package com.yootk.action;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/pages/member/")
public class MemberAction {
    @GetMapping("info")
    @ResponseBody
    public Object info() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication;
    }
}

2、
localhost/pages/member/info

3、
    @GetMapping("principal")
    @ResponseBody
    public Object principal(Principal principal) {
        return principal; // 直接返回对象
    }

SpringSecurity 标签支持

SpringSecurity 核心的标签

  • 用户认证成功后,每一个用户的数据信息都被保存在了 Authentcation 接口实例之中但是在现实的应用开发中,需要根据用户对应的授权项动态的生成前台链接,所以为了便于视图层的操作,SpringSecurity 提供了两个核心的标签:
  :获取当前认证用户的信息:<security:authentication>"'<security:authorize>”
  :基于 SpEL 实现授权信息检测;
1、
package com.yootk.action;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainAction {
    @GetMapping("/main")
    public Object main(){
        return "/main"; // JSP页面
    }
}


2、
<%@ page pageEncoding="UTF-8"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
<h1>登录用户名:<security:authentication property="principal.username"/></h1>

3、
<%@ page pageEncoding="UTF-8"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
<security:authorize access="isAuthenticated()">
    <h1>登录用户名:<security:authentication property="principal.username"/></h1>
</security:authorize>

4、
<%@ page pageEncoding="UTF-8"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
<security:authorize access="isAuthenticated()">
    <h1>登录用户名:<security:authentication property="principal.username"/></h1>
</security:authorize>
<security:authorize access="hasRole('MESSAGE')">
    <h1>该用户拥有”MESSAGE“消息管理权限!</h1>
</security:authorize>
<security:authorize access="hasRole('ADMIN')">
    <h1>该用户拥有”ADMIN“最高系统管理权限!</h1>
</security:authorize>

SpringSecurity 注解支持

@EnableGlobalMethodSecurity 注解

  • 由于 SpringSecurity 的技术发展的不同阶段,以及对 JSR-250 的支持,实际上对于认证与授权注解一共分为了三种不同的支持,而要想使用这些注解,则还需要通过@EnableGlobalMethodSecurity 注解进行启用配置
1、
// https://mvnrepository.com/artifact/jakarta.annotation/jakarta.annotation-api
implementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '2.1.1'


2、
package com.yootk.config;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@EnableGlobalMethodSecurity( // 启用业务层方法的认证与授权检测
        prePostEnabled = true) // @PreAuthorize与@PostAuthorize注解启用
public class MethodSecurityConfig { // 方法安全控制
}


3、
package com.yootk.service;

import org.springframework.security.access.prepost.PreAuthorize;

public interface IAdminService { // 接口上直接定义所需要的注解
    @PreAuthorize("hasRole('ADMIN')") // 业务调用之前进行授权检查
    public boolean add();
    // 此时要同时满足有两个角色存在,才允许访问该业务接口
    @PreAuthorize("hasRole('ADMIN') AND hasRole('SYSTEM')")  // 业务调用之前检查
    public boolean edit();
}


4、
package com.yootk.service.impl;

import com.yootk.service.IAdminService;
import org.springframework.stereotype.Service;

@Service
public class AdminServiceImpl implements IAdminService {
    @Override
    public boolean add() {
        return false;
    }
    @Override
    public boolean edit() {
        return false;
    }
}


5、
package com.yootk.action;

import com.yootk.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Map;

@Controller
@RequestMapping("/pages/admin/") // 父路径没有进行安全检查
public class AdminAction {
    @Autowired
    private IAdminService adminService; // 注入业务接口实例

    @ResponseBody
    @GetMapping("add")
    public Object add() {
        return Map.of("result", "创建新的管理员", "flag", this.adminService.add());
    }
    @ResponseBody
    @GetMapping("edit")
    public Object edit() {
        return Map.of("result", "修改管理员数据", "flag", this.adminService.edit());
    }
}


6、
localhost/pages/admin/add

7、
localhost/pages/admin/edit

8、
localhost/pages/admin/delete?ids=muyan&ids=yootk&ids=lee

9、
    @ResponseBody
    @GetMapping("delete")
    public Object delete(@RequestParam(value = "ids", required = false) List<String> ids) {
        return Map.of("result", this.adminService.delete(ids));
    }

10、

localhost/pages/admin/delete?ids=muyan&ids=hello&ids=lee

11、
    @Secured({"ROLE_ADMIN", "ROLE_MESSAGE"})
    public String get(String username);


12、
localhost/pages/admin/get?username=yootk

13、
package com.yootk.config;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@EnableGlobalMethodSecurity( // 启用业务层方法的认证与授权检测
        prePostEnabled = true, // @PreAuthorize与@PostAuthorize注解启用
        securedEnabled = true,  // @Secured
        jsr250Enabled = true) // JSR-250安全注解标准
public class MethodSecurityConfig { // 方法安全控制
}

14、
package com.yootk.service;

import jakarta.annotation.security.RolesAllowed;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;

import java.util.List;

public interface IAdminService { // 接口上直接定义所需要的注解
    @PreAuthorize("hasRole('ADMIN')") // 业务调用之前进行授权检查
    public boolean add();
    // 此时要同时满足有两个角色存在,才允许访问该业务接口
    @PreAuthorize("hasRole('ADMIN') AND hasRole('SYSTEM')")  // 业务调用之前检查
    public boolean edit();
    // 只有传递的参数的内部包含有”yootk“字符串的操作才允许你进行业务调用
    @PreFilter(filterTarget = "ids", value = "filterObject.contains('yootk')")
    public Object delete(List<String> ids);
    @Secured({"ROLE_ADMIN", "ROLE_MESSAGE"})
    public String get(String username);
    @RolesAllowed({"MESSAGE", "SYSTEM"}) // 拥有其中的一个权限即可访问
    public Object list();
}

15、
    @ResponseBody
    @GetMapping("list")
    public Object list() {
        return Map.of("result", this.adminService.list());
    }

16、
localhost/pages/admin/list

CSRF 访问控制

CSRF 漏洞攻击

  • WEB 应用由于部署在公网之中,并且客户端只需要通过浏览器即可访问,那么在客户端上就会保存有不同 WEB 站点的服务信息,而这时就有可能会产生 CSRF 漏洞。CSRF(Cross-site request forgery、跨站请求伪造)是一种常见的网络攻击模式,攻击者可以在受害者完全不知情的情况下以受害者的身份进行各种请求的发送(例如:邮件处理帐号操作等),并且在服务器看来这些操作全部都属于合法访问

Token 标记验证

  • 如果要想解决 CSRF 的安全漏洞,最简单的形式就是追加一些验证的标记信息,所以按照此设计思路,在实际的开发中有三种解决方案:验证 HTTP 请求头信息中的“Referer”信息、在访问路径中追加 token 标记、在 HTTP 信息头中定义验证属性,这样服务端在每次处理客户端请求之前,进行该标记的验证处理即可

关闭 CSRF

http.csrf().disable(); // 关闭 CSRF 校验

1、
package com.yootk.action;

import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/pages/message/")
public class MessageAction {
    @GetMapping("input")
    public String input() {
        return "/message/input"; // 输入表单路径
    }
    @PostMapping("echo")
    @ResponseBody
    public Object echo(String msg) {
        return Map.of("result", "【ECHO】" + msg);
    }
}


2、
<%@ page pageEncoding="UTF-8"%>
<%!
    public static final String ECHO_URL = "/pages/message/echo"; // 请求路径
%>
<form action="<%=ECHO_URL%>" method="POST">
    消息内容:<input type="text" name="msg" value="江湖人称:爆可爱的小李老师"/>
    <button type="submit">发送</button>
</form>

3、
localhost/pages/message/input

4、
<%@ page pageEncoding="UTF-8"%>
<%!
    public static final String ECHO_URL = "/pages/message/echo"; // 请求路径
%>
<form action="<%=ECHO_URL%>" method="POST">
    消息内容:<input type="text" name="msg" value="江湖人称:爆可爱的小李老师"/>
    <input type="hidden" name="${ _csrf.parameterName }" value="${ _csrf.token }">
    <button type="submit">发送</button>
</form>

5、

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                .access("isAuthenticated()") // 只允许认证后的用户访问
                .antMatchers("/pages/message/**")
                .access("hasAnyRole('ADMIN', 'NEWS', 'MESSAGE')") // 任意一个角色都可以访问
                .antMatchers("/pages/news/**") // 路径不存在
                .access("hasRole('NEWS')")
                .antMatchers("/**").access("permitAll");
        http.formLogin(); // SpringSecurity内部自带登录表单
        http.csrf().disable(); // 关闭CSRF校验
        return http.build();
    }

扩展登录与注销功能

自定义登录注销处理

  • 虽然 SpringSecurity 内置了用户登录页,但是从实际的开发来讲,其所提供的登录页面显式效果是无法满定实际应用需求的,这样在进行应用开发时,往往就需要开发者自定义登录页。同时考虑到用户操作的便捷性,在登录成功后也需要跳转到指定的首页进行功能展示

1、
<%@ page pageEncoding="UTF-8"%>
<%!
    public static final String LOGIN_URL = "/yootk-login"; // 自定义登录处理页
%>
<h1>${param.error ? "用户登录失败,错误的用户名或密码!" : ""}</h1>
<form action="<%=LOGIN_URL%>" method="post">
    用户名:<input type="text" name="mid" value="yootk"><br/>
    密码:<input type="password" name="pwd" value="hello"><br/>
    <button type="submit">登录</button><button type="reset">重置</button>
</form>

2、
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                .access("isAuthenticated()") // 只允许认证后的用户访问
                .antMatchers("/pages/message/**")
                .access("hasAnyRole('ADMIN', 'NEWS', 'MESSAGE')") // 任意一个角色都可以访问
                .antMatchers("/pages/news/**") // 路径不存在
                .access("hasRole('NEWS')")
                .antMatchers("/**").access("permitAll");
        http.formLogin()
                .usernameParameter("mid") // 用户名提交参数名称
                .passwordParameter("pwd") // 密码的提交参数名称
                .successForwardUrl("/") // 登录成功路径
                .loginPage("/login_page") // 登录表单路径
                .loginProcessingUrl("/yootk-login") // 表单提交路径
                .failureForwardUrl("/login_page?error=登录失败,错误的用户名或密码!") // 登录失败页
                .and()
                        .logout() // 注销配置
                .logoutUrl("/yootk-logout") // 注销路径
                .logoutSuccessUrl("/logout_page") // 注销后的显示路径
                .deleteCookies("JSESSIONID") // 删除Cookie信息
                .and()
                .exceptionHandling()// 认证错误配置
                .accessDeniedPage("/error_403"); // 失败处理
        http.csrf().disable(); // 关闭CSRF校验
        return http.build();
    }

3、
package com.yootk.action;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class CommonAction {
    @RequestMapping("/")
    public String index() {
        return "/index"; // 跳转的JSP页面
    }
    @GetMapping("/login_page")
    public String loginPage() { // 登录表单页
        return "/login";
    }
    @GetMapping("/logout_page")
    public String logout(Model model) { // 注销显示页面
        model.addAttribute("msg", "用户注销成功,欢迎下次访问");
        return "/index";
    }
    @GetMapping("/error_403")
    public String errorPage403() { // 错误显示页
        return "/common/error_page_403"; // 错误的路径
    }
}


4、
<%@ page pageEncoding="UTF-8"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
<%!
    public static final String LOGIN_URL = "/login_page";
    public static final String LOGOUT_URL = "/yootk-logout";
%>
<h3>${msg}</h3>
<security:authorize access="isAuthenticated()">
    <h3>【已登录】登录用户名:<security:authentication property="principal.username"/></h3>
    <h3>登录成功,欢迎您回来,可以选择<a href="<%=LOGOUT_URL%>">注销</a>!</h3>
</security:authorize>
<h3>如果您还未登录,请先<a href="<%=LOGIN_URL%>">登录</a>。</h3>

5、
localhost

过滤器

FilterChainProxy 过滤链代理

  • SpringSecurity 的运行机制主要依靠的是 WEB 过滤器进行触发的,开发者只要在 WEB 中配置 DelegatingFilterProxy 过滤代理类。这样在每次用户发出请求时,都会自动的利用 FilterChainProxy 代理链实现类进行内置过滤器的调用,从实现完整的安全逻辑处理

1、
http.authorizeRequests()

2、
http.formLogin()

3、
http.csrf()

4、
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {}
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
      implements ApplicationEventPublisherAware, MessageSourceAware {}
public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,
      EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {}
public interface Filter {}

Session 并行管理

Session 并行管理

  • 账户是 SpringSecurity 实现认证与授权的核心单元,但是由于所有的系统都保存在公网-,这样任何的使用者只要拥有了账户数据,理论上都可以实现 WEB 应用数据的获取。而一旦用户的账户泄露,就会出现有较为严重的安全隐患,造成核心数据的丢失

1、
        http.sessionManagement() // 获取Session管理配置
                .maximumSessions(1) // 一个账户并行数量为1
                .maxSessionsPreventsLogin(false) // 剔除之前登录过的
                .expiredUrl("/?invalidate=true"); // session失效后的显示页面

2、
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher(); // 剔除已登录用户
}


3、

<h3>${param.invalidate ? "当前账户已在其他设备登录,为了您的安全已经该账户注销。" : ""}</h3>
4、

RememberMe

SpringSecuryt 记住我功能

  • 在应用中启用认证与授权检测的目的是处于对核心资源的保护,但是在一个用户频繁使用的系统之中,如果每次都重复的进行用户登录的处理操作,则也会严重的影响该系统的用户感官。为了简化这一操作的机制,可以基于 Cookie 实现登录认证处理的 Token 数据存储,在每次访问时首先通过 Cookie 数据检查用户登录状态,如果已登录过则直接访问所需资源,而如果未登录过的用户则跳转到登录页面进行正常的登录处理,操作流程如图所示。这样就可以减少用户重复输入用户名和密码的次数,便于用户的使用

Token 数据持久化

  • 此时的所有用户的登录 Token 数据,实际上都会保存在服务器端内存之中这样随着免登录用户的数量增加,必然会增加额外的内存开销。所以为了解决该问题 SpringSecurity 又提供了一个数据库存储 Token 的支持

Token 存储设计结构

  • 为了实现 Token 数据的管理,SpringSecurity 的内部提供了一个 PersistentTokenRepository 操作接口,同时又提供了内存 Token 存储实现子类(InMemoryTokenRepositorylmpl)与数据库 Token 存储实现子类(JdbcTokenRepositorympl),在没有进行任何配置时,RememberMeConfigurer 会使用内存方式存储 Token, 除非用户明确的设置了数据库存储,才会进行存储介质的更换,
1、
<%@ page pageEncoding="UTF-8"%>
<%!
    public static final String LOGIN_URL = "/yootk-login"; // 自定义登录处理页
%>
<h1>${param.error ? "用户登录失败,错误的用户名或密码!" : ""}</h1>
<form action="<%=LOGIN_URL%>" method="post">
    用户名:<input type="text" name="mid" value="yootk"><br/>
    密码:<input type="password" name="pwd" value="hello"><br/>
    <input type="checkbox" id="rme" name="rme" value="true" checked>下次免登录<br/>
    <button type="submit">登录</button><button type="reset">重置</button>
</form>

2、
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
                                           UserDetailsService userDetailsService) throws Exception {
        http.sessionManagement() // 获取Session管理配置
                .maximumSessions(1) // 一个账户并行数量为1
                .maxSessionsPreventsLogin(false) // 剔除之前登录过的
                .expiredUrl("/?invalidate=true"); // session失效后的显示页面
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                .access("isAuthenticated()") // 只允许认证后的用户访问
                .antMatchers("/pages/message/**")
                .access("hasAnyRole('ADMIN', 'NEWS', 'MESSAGE')") // 任意一个角色都可以访问
                .antMatchers("/pages/news/**") // 路径不存在
                .access("hasRole('NEWS')")
                .antMatchers("/**").access("permitAll");
        http.formLogin()
                .usernameParameter("mid") // 用户名提交参数名称
                .passwordParameter("pwd") // 密码的提交参数名称
                .successForwardUrl("/") // 登录成功路径
                .loginPage("/login_page") // 登录表单路径
                .loginProcessingUrl("/yootk-login") // 表单提交路径
                .failureForwardUrl("/login_page?error=登录失败,错误的用户名或密码!") // 登录失败页
                .and()
                        .logout() // 注销配置
                .logoutUrl("/yootk-logout") // 注销路径
                .logoutSuccessUrl("/logout_page") // 注销后的显示路径
                .deleteCookies("JSESSIONID", "yootk-cookie-rememberme") // 删除Cookie信息
                .and()
                .exceptionHandling()// 认证错误配置
                .accessDeniedPage("/error_403"); // 失败处理
        http.csrf().disable(); // 关闭CSRF校验
        http.rememberMe()
                .userDetailsService(userDetailsService) // 获取用户信息
                .rememberMeParameter("rme") // 参数名称
                .key("yootk-lixinghua") // 密钥
                .tokenValiditySeconds(2_592_000) // Cookie保存时间,30天免登录
                .rememberMeCookieName("yootk-cookie-rememberme"); // Cookie名称
        return http.build();
    }

3、
localhost/login

4、
localhost/pages/message/info

5、
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                .fullyAuthenticated() // 强制性认证
                .antMatchers("/pages/news/**") // 路径不存在
                .access("hasRole('NEWS')")
                .antMatchers("/**").access("permitAll");

6、
https://docs.spring.io/spring-security/reference/6.0/servlet/authentication/rememberme.html

7、
USE yootk_java;
create table persistent_logins (username varchar(64) not null,
								series varchar(64) primary key,
								token varchar(64) not null,
								last_used timestamp not null);

8、
yootk.database.dirverClassName=com.mysql.cj.jdbc.Driver
yootk.database.jdbcUrl=jdbc:mysql://localhost:3306/yootk
yootk.database.username=root
yootk.database.password=mysqladmin
yootk.database.connectionTimeOut=3000
yootk.database.readOnly=false
yootk.database.pool.idleTimeOut=3000
yootk.database.pool.maxLifetime=60000
yootk.database.pool.maximumPoolSize=60
yootk.database.pool.minimumIdle=20


9、

package com.yootk.config;
@Configuration 						// 配置类
@PropertySource("classpath:config/database.properties") 	// 配置加载
public class DataSourceConfig { 				// 数据库配置Bean
    @Value("${yootk.database.dirverClassName}") 		// 资源文件读取配置项
    private String driverClassName; 			// 数据库驱动程序
    @Value("${yootk.database.jdbcUrl}") 			// 资源文件读取配置项
    private String jdbcUrl; 				// 数据库连接地址
    @Value("${yootk.database.username}") 			// 资源文件读取配置项
    private String username; 				// 用户名
    @Value("${yootk.database.password}") 			// 资源文件读取配置项
    private String password; 				// 密码
    @Value("${yootk.database.connectionTimeOut}") 		// 资源文件读取配置项
    private long connectionTimeout; 			// 连接超时
    @Value("${yootk.database.readOnly}") 			// 资源文件读取配置项
    private boolean readOnly; 				// 只读配置
    @Value("${yootk.database.pool.idleTimeOut}") 		// 资源文件读取配置项
    private long idleTimeout; 				// 连接最小维持时长
    @Value("${yootk.database.pool.maxLifetime}") 		// 资源文件读取配置项
    private long maxLifetime; 				// 连接最大存活时长
    @Value("${yootk.database.pool.maximumPoolSize}") 	// 资源文件读取配置项
    private int maximumPoolSize; 				// 连接池最大维持数量
    @Value("${yootk.database.pool.minimumIdle}") 		// 资源文件读取配置项
    private int minimumIdle; 				// 连接池最小维持数量
    @Bean("dataSource") 					// Bean注册
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();// DataSource子类实例化
        dataSource.setDriverClassName(this.driverClassName); // 驱动程序
        dataSource.setJdbcUrl(this.jdbcUrl); 		// JDBC连接地址
        dataSource.setUsername(this.username); 		// 用户名
        dataSource.setPassword(this.password); 		// 密码
        dataSource.setConnectionTimeout(this.connectionTimeout); // 连接超时
        dataSource.setReadOnly(this.readOnly); 		// 是否为只读数据库
        dataSource.setIdleTimeout(this.idleTimeout); 	// 最小维持时间
        dataSource.setMaxLifetime(this.maxLifetime); 	// 连接的最大时长
        dataSource.setMaximumPoolSize(this.maximumPoolSize); // 连接池最大容量
        dataSource.setMinimumIdle(this.minimumIdle); 	// 最小维持连接量
        return dataSource; 				// 返回Bean实例
    }
}

10、
package com.yootk.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

import javax.sql.DataSource;

@Configuration
public class TokenRepositoryConfig {
    @Bean
    public JdbcTokenRepositoryImpl jdbcTokenRepository(DataSource dataSource) {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    }
}

11、
   @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
                                           UserDetailsService userDetailsService,
                                           JdbcTokenRepositoryImpl tokenRepository) throws Exception {
        http.sessionManagement() // 获取Session管理配置
                .maximumSessions(1) // 一个账户并行数量为1
                .maxSessionsPreventsLogin(false) // 剔除之前登录过的
                .expiredUrl("/?invalidate=true"); // session失效后的显示页面
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                .fullyAuthenticated() // 强制性认证
                .antMatchers("/pages/news/**") // 路径不存在
                .access("hasRole('NEWS')")
                .antMatchers("/**").access("permitAll");
        http.formLogin()
                .usernameParameter("mid") // 用户名提交参数名称
                .passwordParameter("pwd") // 密码的提交参数名称
                .successForwardUrl("/") // 登录成功路径
                .loginPage("/login_page") // 登录表单路径
                .loginProcessingUrl("/yootk-login") // 表单提交路径
                .failureForwardUrl("/login_page?error=登录失败,错误的用户名或密码!") // 登录失败页
                .and()
                        .logout() // 注销配置
                .logoutUrl("/yootk-logout") // 注销路径
                .logoutSuccessUrl("/logout_page") // 注销后的显示路径
                .deleteCookies("JSESSIONID", "yootk-cookie-rememberme") // 删除Cookie信息
                .and()
                .exceptionHandling()// 认证错误配置
                .accessDeniedPage("/error_403"); // 失败处理
        http.csrf().disable(); // 关闭CSRF校验
        http.rememberMe()
                .tokenRepository(tokenRepository) // JDBC数据存储
                .userDetailsService(userDetailsService) // 获取用户信息
                .rememberMeParameter("rme") // 参数名称
                .key("yootk-lixinghua") // 密钥
                .tokenValiditySeconds(2_592_000) // Cookie保存时间,30天免登录
                .rememberMeCookieName("yootk-cookie-rememberme"); // Cookie名称
        return http.build();
    }

验证码保护

验证码处理逻辑

  • 由于现代网络应用环境的复杂,为了保障个人账户的安全性,在用户登录时除了要求进行账户信息的输入,还会额外的要求验证码的处理,甚至在更加严格的安全环境下,还会对当前登陆者的手机(手机验证码),以及当前设备的编号进行验证处理

验证码过滤器处理实现

  • 由于登录验证码的检测需要在用户登录认证过滤之前进行处理,所以本次可以考虑在已有的 SpringSecurity 过滤链之中追加一个验证码过滤器。由于 JakartaEE 标准中 Fitler 为过滤器的实现接口,在定义该过滤器时,用户可以直接实现 Filter 接口,也可以根据需要选择继承 SpringSecurity 中提供的抽象类的方式进行定义。
1、
package com.yootk.util;
public class CaptchaUtil { 				// 验证码工具类
    private static final String[] CANDIDATE_DATA = {
            "A", "B", "C", "D", "E", "F", "G", "H", "J", "K",
            "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X",
            "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
            "k", "m", "n", "p", "s", "t", "u", "v", "w", "x", "y", "z",
            "1", "2", "3", "4", "5", "6", "7", "8", "9" }; 	// 候选数据
    public static final String CAPTCHA_NAME = "yootk-captcha"; // 验证码名称
    private static final int WIDTH = 80; 			// 生成图片宽度
    private static final int HEIGHT = 25; 			// 生成图片高度
    private static final int LENGTH = 4; 			// 验证码长度
    public static void outputCaptcha() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getRequest();	// 获取request对象
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getResponse();	// 获取response对象
        HttpSession session = request.getSession();		// 获取session对象
        response.setHeader("Pragma", "No-cache");  		// 设置页面不缓存
        response.setHeader("Cache-Control", "no-cache"); 	// 设置页面不缓存
        response.setDateHeader("Expires", 0);  		// 缓存失效时间
        response.setContentType("image/png"); 		// 图像显式
        BufferedImage image = new BufferedImage(WIDTH, HEIGHT,
                BufferedImage.TYPE_INT_RGB); 			// 内存中创建图象
        Graphics g = image.getGraphics();			// 获取图形上下文对象
        Random random = new Random();  			// 实例化随机数类
        g.setColor(getRandColor(200, 250));  		// 设定背景色
        g.fillRect(0, 0, WIDTH, HEIGHT);  			// 绘制矩形
        g.setFont(new Font("宋体", Font.PLAIN, 18)); 		// 设定字体
        g.setColor(getRandColor(160, 200)); 		// 获取新的颜色
        for (int i = 0; i < 155; i++) { 			// 产生干扰线
            int x = random.nextInt(WIDTH);
            int y = random.nextInt(HEIGHT);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl); 		// 绘制长线
        }
        StringBuffer sRand = new StringBuffer();		// 保存生成的随机数
        String str [] = captchaData();			// 获取验证码候选数据
        for (int i = 0; i < LENGTH; i++) {  			// 生成4位随机数
            String rand = str[random.nextInt(str.length)]; 	// 获取随机数
            sRand.append(rand); 				// 随机数保存
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110),
                    20 + random.nextInt(110))); 		// 验证码显式
            g.drawString(rand, 16 * i + 6, 19);  		// 图形绘制
        }
        session.setAttribute(CAPTCHA_NAME, sRand.toString());// 验证码存储
        g.dispose();					// 图象生效
        try {
            ImageIO.write(image, "JPEG", response.getOutputStream());// 输出图象到页面
        } catch (IOException e) {}
    }
    private static String[] captchaData() { 		// 验证码候选数据
        return CANDIDATE_DATA;
    }
    private static Color getRandColor(int fc, int bc) { 	// 获取随机颜色
        Random random = new Random();
        if (fc > 255) { 					// 设置颜色边界
            fc = 255;
        }
        if (bc > 255) { 					// 设置颜色边界
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);  		// 随机生成红色数值
        int g = fc + random.nextInt(bc - fc);  		// 随机生成绿色数值
        int b = fc + random.nextInt(bc - fc); 		// 随机生成蓝色数值
        return new Color(r, g, b); 				// 随机返回颜色对象
    }
}


2、
package com.yootk.action;

import com.yootk.util.CaptchaUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class CaptchaAction {
    @RequestMapping("/captcha")
    public ModelAndView captcha() {
        CaptchaUtil.outputCaptcha(); // 生成验证码
        return null;
    }
}


3、
localhost/captcha

4、

package com.yootk.exception;

import org.springframework.security.core.AuthenticationException;

public class CaptchaException extends AuthenticationException {
    public CaptchaException(String msg) {
        super(msg);
    }
}

5、
package com.yootk.filter;

import com.yootk.exception.CaptchaException;
import com.yootk.util.CaptchaUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class CaptchaAuthenticationFilter extends OncePerRequestFilter { // 执行一次处理
    private String codeParameter = "code"; // 验证码参数名称
    private AuthenticationFailureHandler authenticationFailureHandler = null; // 失败处理
    public void setAuthenticationFailureHandler(
            AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if ("/yootk-login".equals(request.getRequestURI()) &&
            "POST".equals(request.getMethod())) { // 登录表单操作
            String captcha = (String) request.getSession().getAttribute(
                    CaptchaUtil.CAPTCHA_NAME); // 获取Session验证码
            String code = request.getParameter(this.codeParameter); // 输入验证码
            if (captcha == null || "".equals(captcha) || code == null ||
                "".equals(code) || !captcha.equalsIgnoreCase(code)) {   // 验证码错误
                this.authenticationFailureHandler.onAuthenticationFailure(
                        request, response, new CaptchaException("错误的验证码"));
            } else {
                filterChain.doFilter(request, response); // 执行下一个过滤
            }
        } else {
            filterChain.doFilter(request, response); // 执行下一个过滤
        }
    }
}


6、
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
                                           UserDetailsService userDetailsService,
                                           JdbcTokenRepositoryImpl tokenRepository,
                                           SimpleUrlAuthenticationFailureHandler failureHandler) throws Exception {
        http.sessionManagement() // 获取Session管理配置
                .maximumSessions(1) // 一个账户并行数量为1
                .maxSessionsPreventsLogin(false) // 剔除之前登录过的
                .expiredUrl("/?invalidate=true"); // session失效后的显示页面
        http.authorizeRequests() // 配置认证的访问请求
                .antMatchers("/pages/message/**")
                .fullyAuthenticated() // 强制性认证
                .antMatchers("/pages/news/**") // 路径不存在
                .access("hasRole('NEWS')")
                .antMatchers("/**").access("permitAll");
        http.formLogin()
                .usernameParameter("mid") // 用户名提交参数名称
                .passwordParameter("pwd") // 密码的提交参数名称
                .successForwardUrl("/") // 登录成功路径
                .loginPage("/login_page") // 登录表单路径
                .loginProcessingUrl("/yootk-login") // 表单提交路径
                .failureForwardUrl("/login_page?error=登录失败,错误的用户名或密码!") // 登录失败页
                .and()
                        .logout() // 注销配置
                .logoutUrl("/yootk-logout") // 注销路径
                .logoutSuccessUrl("/logout_page") // 注销后的显示路径
                .deleteCookies("JSESSIONID", "yootk-cookie-rememberme") // 删除Cookie信息
                .and()
                .exceptionHandling()// 认证错误配置
                .accessDeniedPage("/error_403"); // 失败处理
        http.csrf().disable(); // 关闭CSRF校验
        http.rememberMe()
                .tokenRepository(tokenRepository) // JDBC数据存储
                .userDetailsService(userDetailsService) // 获取用户信息
                .rememberMeParameter("rme") // 参数名称
                .key("yootk-lixinghua") // 密钥
                .tokenValiditySeconds(2_592_000) // Cookie保存时间,30天免登录
                .rememberMeCookieName("yootk-cookie-rememberme"); // Cookie名称
        CaptchaAuthenticationFilter captchaAuthenticationFilter =
                new CaptchaAuthenticationFilter();
        captchaAuthenticationFilter.setAuthenticationFailureHandler(failureHandler);
        // 验证码应该在用户登录认证之前来进行检查
        http.addFilterBefore(captchaAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

7、

<%@ page pageEncoding="UTF-8"%>
<%!
    public static final String LOGIN_URL = "/yootk-login"; // 自定义登录处理页
%>
<h1>${param.error ? "用户登录失败,错误的用户名或密码!" : ""}</h1>
<form action="<%=LOGIN_URL%>" method="post">
    用户名:<input type="text" name="mid" value="yootk"><br/>
    密码:<input type="password" name="pwd" value="hello"><br/>
    <input type="checkbox" id="rme" name="rme" value="true" checked>下次免登录<br/>
    验证码:<input type="text" name="code" maxlength="4" size="4"><img src="/captcha"><br/>
    <button type="submit">登录</button><button type="reset">重置</button>
</form>

投票器概述

投票器访问

  • 传统 WEB 开发之中,如果用户要访问核心资源,那么所有的请求必须经过 SpringSecurity 安全检测后,才可以进行访问,但是随着不同应用管理的复杂性,有可能有些站点会对一部分特定的用户有所区分,此时为了简化该类型用户的访问,SpringSecurity 提供了投票器的资源访问策略。

SpringSecurity 投票策略

  • 投票访问策略指的就是设置一些基础的逻辑判断,每一个逻辑判断都可以理解为-个投票器,若干个投票器一起进行访问验证的逻辑处理,当符合投票器既定的策略后,该用户就可以访问,反之则禁止用户进行访问。SpringSecurity 为了便于投票策略的管理,提供了投票管理器(AccessDecisionManager)以及投票者(AccessDecisionVoter)的处理接口
1、
package com.yootk.voter;

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import java.util.Collection;
import java.util.Iterator;

public class LocalAccessVoter implements AccessDecisionVoter<FilterInvocation> {
    // hasRole()、hasAnyRole()里面都有一系列的标记
    private static final String LOCAL_FLAG = "LOCAL_IP"; // 设置本地访问的标记
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return attribute != null && attribute.toString().contains(LOCAL_FLAG);
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.equals(clazz);
    }

    @Override
    public int vote(Authentication authentication, FilterInvocation object,
                    Collection<ConfigAttribute> attributes) {
        if (!(authentication.getDetails()
                instanceof WebAuthenticationDetails)) { // 不是来自于WEB访问
            return AccessDecisionVoter.ACCESS_DENIED; // 反对
        }
        WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
        String ip = details.getRemoteAddress(); // 获取用户的IP地址
        Iterator<ConfigAttribute> iter = attributes.iterator();
        while (iter.hasNext()) {
            ConfigAttribute ca = iter.next(); // 获取配置属性
            if (ca.toString().equals(LOCAL_FLAG)) {
                if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
                    return AccessDecisionVoter.ACCESS_GRANTED; // 通过
                }
            }
        }
        return AccessDecisionVoter.ACCESS_ABSTAIN; //  弃权
    }
}


2、
    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<? extends Object>> decisionVoters =
                new ArrayList<>();
        decisionVoters.add(new RoleVoter());
        decisionVoters.add(new AuthenticatedVoter());
        decisionVoters.add(new LocalAccessVoter());
        decisionVoters.add(new WebExpressionVoter());
        return new AffirmativeBased(decisionVoters);
    }

3、
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
                                           UserDetailsService userDetailsService,
                                           JdbcTokenRepositoryImpl tokenRepository,
                                           SimpleUrlAuthenticationFailureHandler failureHandler,
                                           AccessDecisionManager accessDecisionManager) throws Exception {
        http.sessionManagement() // 获取Session管理配置
                .maximumSessions(1) // 一个账户并行数量为1
                .maxSessionsPreventsLogin(false) // 剔除之前登录过的
                .expiredUrl("/?invalidate=true"); // session失效后的显示页面
        http.authorizeRequests() // 配置认证的访问请求
                .accessDecisionManager(accessDecisionManager) // 投票器
                .antMatchers("/pages/message/**")
                .access("hasAnyRole('ADMIN', 'MESSAGE') or hasRole('LOCAL_IP')")
                .antMatchers("/pages/news/**") // 路径不存在
                .access("hasRole('NEWS')")
                .antMatchers("/**").access("permitAll");
        http.formLogin()
                .usernameParameter("mid") // 用户名提交参数名称
                .passwordParameter("pwd") // 密码的提交参数名称
                .successForwardUrl("/") // 登录成功路径
                .loginPage("/login_page") // 登录表单路径
                .loginProcessingUrl("/yootk-login") // 表单提交路径
                .failureForwardUrl("/login_page?error=登录失败,错误的用户名或密码!") // 登录失败页
                .and()
                        .logout() // 注销配置
                .logoutUrl("/yootk-logout") // 注销路径
                .logoutSuccessUrl("/logout_page") // 注销后的显示路径
                .deleteCookies("JSESSIONID", "yootk-cookie-rememberme") // 删除Cookie信息
                .and()
                .exceptionHandling()// 认证错误配置
                .accessDeniedPage("/error_403"); // 失败处理
        http.csrf().disable(); // 关闭CSRF校验
        http.rememberMe()
                .tokenRepository(tokenRepository) // JDBC数据存储
                .userDetailsService(userDetailsService) // 获取用户信息
                .rememberMeParameter("rme") // 参数名称
                .key("yootk-lixinghua") // 密钥
                .tokenValiditySeconds(2_592_000) // Cookie保存时间,30天免登录
                .rememberMeCookieName("yootk-cookie-rememberme"); // Cookie名称
        CaptchaAuthenticationFilter captchaAuthenticationFilter =
                new CaptchaAuthenticationFilter();
        captchaAuthenticationFilter.setAuthenticationFailureHandler(failureHandler);
        // 验证码应该在用户登录认证之前来进行检查
        http.addFilterBefore(captchaAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

4、
localhost/pages/message/info

本地 IP 直接访问

IP 直接访问投票处理

  • 为了便于 WEB 数据的维护以及监控处理操作,可以将一些管理员的主机 IP 进行固定,这样只要程序发现当前所访问应用为指定 IP 时,就可以绕开 SpringSecurity 的安全管理机制,而直接获取所需要的 WEB 资源

配置投票管理器

  • 本次由于要进行 IP 地址的直接访问处理操作,所以可以使用 AffirmativeBased 子类实现投票管理器实例的配置,而要想在 SpringSecurity 中使用投票管理器,则需要通过 HttpSecurity 类提供的 accessDecisionManager()方法进行设置,该方法会返回 ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry 对象实例,并通过该实例结合 Access 表达式进行标记配置即可

RoleHierarchy

1、
    @Bean
    public RoleHierarchy roleHierarchy() { // 角色的层级关系
        RoleHierarchyImpl role = new RoleHierarchyImpl();
        role.setHierarchy("ROLE_ADMIN > ROLE_RESOURCE > ROLE_USER");
        return role;
    }

2、
    @Bean
    public SecurityExpressionHandler<FilterInvocation> expressionHandler(
            RoleHierarchy roleHierarchy) {
        DefaultWebSecurityExpressionHandler expressionHandler =
                new DefaultWebSecurityExpressionHandler();
        expressionHandler.setRoleHierarchy(roleHierarchy);
        return expressionHandler;
    }

3、
    @Bean
    public AccessDecisionManager accessDecisionManager(
            SecurityExpressionHandler expressionHandler) {
        List<AccessDecisionVoter<? extends Object>> decisionVoters =
                new ArrayList<>();
        decisionVoters.add(new RoleVoter());
        decisionVoters.add(new AuthenticatedVoter());
        decisionVoters.add(new LocalAccessVoter());
        decisionVoters.add(new WebExpressionVoter());
        WebExpressionVoter webExpressionVoter = new WebExpressionVoter(); // 投票器
        webExpressionVoter.setExpressionHandler(expressionHandler); // 表达式
        decisionVoters.add(webExpressionVoter); // 追加新的投票器
        return new AffirmativeBased(decisionVoters);
    }

4、
        http.authorizeRequests() // 配置认证的访问请求
                .accessDecisionManager(accessDecisionManager) // 投票器
                .antMatchers("/pages/message/**")
                .access("hasRole('RESOURCE')")

5、
http://localhost/pages/message/info

demo


上次编辑于: