1,基本内容

1.1,常见问题

【问题】JSP的过滤器,SpringMVC的拦截器,以及SpringSecurity的区别?

【答案】拦截器:

  • JSP的过滤器(Filter):JSP的过滤器是Servlet规范中定义的一种对象,用于在请求到达Servlet之前或响应离开Servlet之前对请求和响应进行预处理和后处理。JSP的过滤器可以用于处理请求参数、请求头、响应内容等,常用于编码转换、权限验证、日志记录等场景。
  • SpringMVC的拦截器(Interceptor):SpringMVC的拦截器是Spring框架中的一种对象,用于在请求到达Controller之前或响应离开Controller之后对请求和响应进行预处理和后处理。SpringMVC的拦截器可以用于处理请求参数、请求头、响应内容等,常用于权限验证、日志记录、性能监控等场景。
  • SpringSecurity:SpringSecurity是Spring框架中的一个安全框架,用于处理认证和授权相关的问题。SpringSecurity提供了一系列的过滤器(Filter)和拦截器(Interceptor)来实现安全功能,如身份验证、授权、会话管理、记住我等功能。SpringSecurity的过滤器和拦截器可以与SpringMVC的拦截器和过滤器结合使用,实现更加完整的安全功能。

1.2,基本原理和简单安全认证

为了提供安全的机制,Spring提供了其安全框架Spring Security,它是一个能够为基于Spring生态圈,提供安全访问控制解决方案的框架。为了对请求进行拦截,Spring Security提供了过滤器DelegatingFilterProxy类给予开发者配置。在传统的Web工程中,可以使用web.xml进行配置,但是因为Spring Boot推荐的是全注解的方式。在Spring全注解的方式下,只需要加入@EnableWebSecurity就可以驱动Spring Sercurity了。而在Spring Boot中,只需要在Maven配置文件中引入对应的依赖,它便会自动启动Spring Sercurity。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>@SpringBootApplication
@EnableWebSecurity
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }
}

【基本原理】在Java Web工程中,一般使用Servlet过滤器(Filter)对请求进行拦截,然后在Filter中通过自己的验证逻辑来决定是否放行请求。同样地,Spring Security也是基于这个原理,在进入到DispatcherServlet前就可以对Spring MVC的请求进行拦截,然后通过一定的验证,从而决定是否放行请求访问系统。一旦启用了Spring Security,Spring IoC容器就会为你创建一个名称为springSecurityFilterChain的Spring Bean。它的类型为FilterChainProxy,事实上它也实现了Filter接口,只是它是一个特殊的拦截器。

在Spring Security操作的过程中它会提供Servlet过滤器DelegatingFilterProxy,这个过滤器会通过Spring Web IoC容器去获取Spring Security所自动创建的FilterChainProxy对象,这个对象上存在一个拦截器列表(List),列表上存在用户验证的拦截器、跨站点请求伪造等拦截器,这样它就可以提供多种拦截功能。于是焦点又落到了FilterChainProxy对象上,通过它还可以注册Filter,也就是允许注册自定义的Filter来实现对应的拦截逻辑,以满足不同的需要。

【验证流程】Security采用的是责任链的设计模式,它有一条很长的过滤器链:

spring aop 注册拦截器 springsecurity拦截器_html

Security本质就是通过一组过滤器来过滤HTTP请求,将HTTP请求转发到不同的处理模块,最后经过业务逻辑处理返回Response的过程。HttpSecurity对象实际提供的是各个过滤器对应的配置类,通过配置类来控制对应过滤器属性的配置,最后将过滤器加载到HttpSecurity的过滤链中。

  • UsernamePasswordAuthenticationFilter:表单登录。
  • BasicAuthenticationFilter:HTTP登录。
  • … :这里还有很多过滤器,可以根据自己的情况加入过滤器。
  • ExceptionTranslationFilter:这个过滤器必须要在FilterSecurityInterceptor的前面,位置不能动,它的作用是处理FilterSecurityInterceptor抛出的异常。
  • FilterSecurityInterceptor:这个过滤器处于整个过滤器链的最后一环,也是进入RESTful程序的前一个程序,这里将会做最后一次验证。

首先,用户输入用户名和密码,然后单击登录。其中绿色部分的每一种过滤器代表着一种认证方式,主要用于检查当前请求有没有涉及用户信息,如果当前没有,就会跳入到下一个绿色的过滤器中,请求成功会显示标记。绿色认证方式可以配置,比如短信认证、微信认证等。如果我们不配置BasicAuthenticationFilter,那么它就不会生效。FilterSecurityInterceptor过滤器是最后一个,它会决定当前的请求可不可以访问RESTful API,判断规则会放在这里面。当不通过时会把异常抛给前面的ExceptionTranslationFilter过滤器。ExceptionTranslationFilter接收异常信息时,将跳转页面引导用户进行认证。橘黄色和蓝色的位置不可更改。当没有认证的request进入过滤器链时,首先进入FilterSecurityInterceptor,判断当前是否进行了认证,如果没有认证则进入ExceptionTranslationFilter,处理抛出的异常,然后跳转到认证页面。

同时,Security提供了多种登录认证的方式,由多种过滤器共同实现,不同的过滤器被加载到应用中,我们可以根据不同的需求自定义登录认证配置。

1.3,核心组件

Spring Security最核心的功能是认证和授权,主要依赖一系列的组件和过滤器相互配合来完成。Spring Security的核心组件包括:SecurityContextHolder、Authentication、AuthenticationManager、UserDetailsService、UserDetails等。

(1)SecurityContextHolder:SecurityContextHolder用于存储应用程序安全上下文(Spring Context)的详细信息,如当前操作的用户对象信息、认证状态、角色权限信息等。默认情况下,SecurityContextHolder使用ThreadLocal来存储这些信息,意味着上下文始终可用在同一执行线程中的方法。例如,获取有关当前用户的信息的方法:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

因为身份信息与线程是绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。例如,获取当前经过身份验证的用户的名称,其中getAuthentication()返回认证信息,getPrincipal()返回身份信息,UserDetails是对用户信息的封装类。

(2)Authentication:Authentication是认证信息接口,集成了Principal类。Authentication定义了getAuthorities()、getCredentials()、getDetails()和getPrincipal()等接口实现认证功能。

spring aop 注册拦截器 springsecurity拦截器_spring_02

(3)AuthenticationManager: AuthenticationManager认证管理器负责验证。认证成功后,AuthenticationManager返回填充了用户认证信息(包括权限信息、身份信息、详细信息等,但密码通常会被移除)的Authentication实例,然后将Authentication设置到SecurityContextHolder容器中。AuthenticationManager接口是认证相关的核心接口,也是发起认证的入口。但它一般不直接认证,其常用实现类ProviderManager内部会维护一个List<AuthenticationProvider>列表,其中存放了多种认证方式,默认情况下,只需要通过一个AuthenticationProvider的认证就可以被认为登录成功。

(4)UserDetails:UserDetails用户信息接口定义最详细的用户信息。

spring aop 注册拦截器 springsecurity拦截器_spring_03

(5)UserDetails: UserDetailsService负责从特定的地方加载用户信息,通常通过JdbcDaoImpl从数据库加载具体实现,也可以通过内存映射InMemoryDaoImpl具体实现。

1.4,配置文件 

【配置文件】启动SpringBoot出现日志,访问 

spring aop 注册拦截器 springsecurity拦截器_自定义_04

 ,出现验证。

Using generated security password: 47ee4d13-ca39-406a-a92c-cb757dac7dbe

在文本框输入用户名(User)为“user”,密码为日志打出的随机密码,然后点击登录(Login)按钮,它就能够跳转到请求路径。

spring aop 注册拦截器 springsecurity拦截器_spring aop 注册拦截器_05

为了更加方便的使用Spring Security,我们在application.properties文件引入配置项:

#自定义用户名和密码 spring.security.user.name=myuser spring.security.user.password=123456

有了安全配置的属性,即使没有加入@EnableWebSecurity,Spring Boot也会根据配置的项自动启动安全机制。只是这里使用用户名“myuser”和密码“123456”就可以登录系统了,这样就可以自定义用户和密码了,不需要再随机生成密码。除这些配置外,Spring Boot还支持:

# SECURITY (SecurityProperties)
# Spring Security过滤器排序
spring.security.filter.order=-100
# 安全过滤器责任链拦截的分发类型
spring.security.filter.dispatcher-types=async,error,request 
# 用户名,默认值为user
spring.security.user.name=user
# 用户密码
spring.security.user.password= 
# 用户角色
spring.security.user.roles=

# SECURITY OAUTH2 CLIENT (OAuth2ClientProperties)
# OAuth提供者详细配置信息
spring.security.oauth2.client.provider.*= #
# OAuth客户端登记信息
spring.security.oauth2.client.registration.*=@Controller
public class SecurityController {
    @RequestMapping("/")
    public String index() {
        return "index";
    }
}

1.5,登录Demo

在实际项目使用过程中,可能有的功能页面不需要进行登录验证,而有的功能页面只有进行登录验证才能访问。

【content.html】Security退出请求默认只支持post。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<body>
<h1>content</h1>
<p>我是登录后才可以看的页面</p>
<form method="post" action="/logout">
  <button type="submit">退出</button>
</form>
</body>
</html>

【index.html】

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<body>
<h1>Hello</h1>
<p>点击 <a th:href="@{/content}">这里</a>进入管理页面</p>
</body>
</html>

【Controller】

@Controller
public class SecurityController {
    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping("/content")
    public String content() {
        return "content";
    }
}

【SecurityConfig】创建Security的配置文件SecurityConfig类,它继承于WebSecurityConfigurerAdapter,现自定义权限验证配置。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and()
                .csrf()
                .ignoringAntMatchers("/logout");
    }
}

  • @EnableWebSecurity:开启Spring Security权限控制和认证功能。
  • antMatchers("/", "/home").permitAll():配置不用登录可以访问的请求。
  • anyRequest().authenticated():表示其他的请求都必须有权限认证。
  • formLogin():定制登录信息。
  • loginPage("/login"):自定义登录地址,若注释掉,则使用默认登录页面。
  • logout():退出功能,Spring Security自动监控了/logout。
  • ignoringAntMatchers("/logout"):Spring Security默认启用了同源请求控制,在这里选择忽略退出请求的同源限制。

2,自定义用户服务

2.1,使用WebSecurityConfigurerAdapter自定义

为了给FilterChainProxy对象加入自定义的初始化,Spring Security提供了SecurityConfigurer接口,通过它就能够实现对Spring Security的配置。只是有了这个接口还不太方便,因为它只是能够提供接口定义的功能,为了更方便,Spring对Web工程还提供了专门的接口WebSecurityConfigurer,并且在这个接口的定义上提供了一个抽象类WebSecurityConfigurerAdapter。开发者通过继承它就能得到Spring Security默认的安全功能。也可以通过覆盖它提供的方法,来自定义自己的安全拦截方案。这里需要研究WebSecurityConfigurerAdapter中默认存在的3个方法,它们是:

/**
* 用来配置用户签名服务,主要是user-details机制,你还可以给予用户赋予角色
* @param auth 签名管理器构造器,用于构建用户具体权限控制
*/
protected void configure(AuthenticationManagerBuilder auth);

/**
* 用来配置Filter链
*@param web  Spring Web Security 对象
*/
public void configure(WebSecurity web);

/**
* 用来配置拦截保护的请求,比如什么请求放行,什么请求需要验证
* @param http http安全请求对象
*/
protected void configure(HttpSecurity http) throws Exception;

对于使用WebSecurity参数的方法主要是配置Filter链的内容,可以配置Filter链忽略哪些内容。WebSecurityConfigurerAdapter提供的是空实现,也就是没有任何的配置。而对于AuthenticationManager Builder参数的方法,则是定义用户(user)、密码(password)和角色(role),在默认的情况下Spring不会为你创建任何的用户和密码,也就是有登录页面而没有可登录的用户。对于HttpSecurity参数的方法,则是指定用户和角色与对应URL的访问权限,也就是开发者可以通过覆盖这个方法来指定用户或者角色的访问权限。在WebSecurityConfigurerAdapter提供的验证方式下满足通过用户验证或者HTTP基本验证的任何请求,Spring Security都会放行。

 在WebSecurityConfigurerAdapter中的方法:

protected void configure(AuthenticationManagerBuilder auth);

是一个用于配置用户信息的方法,在Spring Security中默认是没有任何用户配置的。而在Spring Boot中,如果没有用户的配置,它将会自动地生成一个名称为user、密码通过随机生成的用户,密码则可以在日志中观察得到。但是这样就存在各类的弊端。为了克服这些弊端,这里先来讨论如何进行自定义用户签名服务。这里主要包含使用:内存签名服务、数据库签名服务和自定义签名服务。关于如何限定请求权限,是通过WebSecurityConfigurerAdapter中的方法configure(HttpSecurity http)来实现的。只是这里在默认的情况下,所有的请求一旦通过验证就会得到放行。

2.2,自定义登录页面

【login.html】

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>login</title>
</head>
<body>
<div th:if="${param.error}">
    用户名或密码错误
</div>
<div th:if="${param.logout}">
    您已注销成功
</div>
<form th:action="@{/login}" method="post">
    <div><label> 用户名 : <input type="text" name="username"/> </label></div>
    <div><label> 密 码 : <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="登录"/></div>
</form>
</body>
</html>

【Controller】

@Controller
public class SecurityController {
    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping("/content")
    public String content() {
        return "content";
    }

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login() {
        return "login";
    }
}

【SecurityConfig】

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and()
                .csrf()
                .ignoringAntMatchers("/logout");
    }
}

3,Security授权

3.1,角色权限

Spring Boot Security实现角色权限控制非常简单,首先配置用户的登录名和密码,然后定义角色和权限,最后关联用户和权限即可。

spring aop 注册拦截器 springsecurity拦截器_自定义_06

【index.html】

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<body>
<h1>Hello</h1>
<p>点击 <a th:href="@{content}">这里</a>进入user页面</p>
<p>点击 <a th:href="@{admin}">这里</a>进入admin页面</p>
</body>
</html>

【SecurityConfig】

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/index.html", "/").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/content/**").access("hasRole('ADMIN') or hasRole('USER')")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                // .loginPage("/login")
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and()
                .csrf()
                .ignoringAntMatchers("/logout");
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("user")
                .password(new BCryptPasswordEncoder().encode("123456")).roles("USER")
                .and()
                .withUser("admin")
                .password(new BCryptPasswordEncoder().encode("admin")).roles("ADMIN", "USER");
    }
}
  • antMatchers("/resources/**", "/").permitAll():地址"/resources/ **"和"/"所有用户都可访问,permitAll表示该请求任何人都可以访问。
  • antMatchers("/admin/**").hasRole("ADMIN"):地址"/admin/**"开头的请求地址,只有拥有ADMIN角色的用户才可以访问。
  • antMatchers("/content/**").access("hasRole('ADMIN') or hasRole('USER')"):地址"/content/**"开头的请求地址,可以供角色为ADMIN或者USER的用户使用。

值得注意的是,hasRole()和access()虽然都可以给角色赋予权限,但有所区别,比如hasRole()修饰的角色"/admin/**",拥有ADMIN权限的用户访问地址xxx/admin和xxx/admin/*均可,如果使用access()修饰的角色,访问地址xxx/admin权限受限,请求xxx/admin/可以通过。

除了permitAll、access这些方法外,Security还提供了更多的权限控制方式:

spring aop 注册拦截器 springsecurity拦截器_spring aop 注册拦截器_07

【1.html】

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
        <title>admin</title>
</head>
<body>
        <h1>admin</h1>
        <p>管理员页面</p>
        <p>点击 <a th:href="@{/}">这里</a> 返回首页</p>
</body>
</html>

【Controller】

@Controller
public class SecurityController {
    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping("/content")
    public String content() {
        return "/content/2.html";
    }

    @RequestMapping("/admin")
    public String admin() {
        return "/admin/1.html";
    }
}

spring aop 注册拦截器 springsecurity拦截器_spring_08

3.2,方法级别的权限控制

在实际项目中,经常会出现一些重要的功能需要控制到按钮级别。在方法上添加注解即可实现限制某些特定功能模块的控制访问权限。如此项目中便可根据角色来控制用户拥有不同的权限。

【@PreAuthorize/@PostAuthorize】Spring Boot提供了@PreAuthorize/@PostAuthorize注解,更适合方法级的权限控制,也支持Spring EL表达式语法,提供了基于表达式的访问控制。

  • @PreAuthorize注解:适合进入方法前的权限验证,@PreAuthorize可以将登录用户的角色/权限参数传到方法中。
  • @PostAuthorize注解:使用并不多,在方法执行后再进行权限验证。
@PreAuthorize("hasAuthority('ADMIN')")
@RequestMapping("/admin")
public String admin() {
   return "admin";
}

【@Secured】此注释用来定义业务方法的安全配置属性的列表,可以在需要安全角色/权限等的方法上指定@Secured,并且只有拥有那些角色/权限的用户才可以调用该方法。如果有人不具备要求的角色/权限,但试图调用此方法,将会抛出AccessDenied异常。

public interface UserService {
    List<User> findAllUsers();

    @Secured("ADMIN")
    void updateUser(User user);

    @Secured({ "USER", "ADMIN" })
    void deleteUser();
}

3.3,强制使用HTTPS

通过HTTPS协议采用证书进行加密,对于那些敏感的信息就可以通过加密进行保护。对于那些需要加密的页面,在Spring中可以强制使用HTTPS请求。

http
    // 使用安全渠道,限定为https请求
    .requiresChannel().antMatchers("/admin/**").requiresSecure()
    // 不使用HTTPS请求
    .and().requiresChannel().antMatchers("/user/**").requiresInsecure()
    // 限定允许的访问角色
    .and().authorizeRequests().antMatchers("/admin/**").hasAnyRole("ADMIN")
    .antMatchers("/user/**").hasAnyRole("ROLE","ADMIN");

这里的requiresChannel方法说明使用通道,然后antMatchers是一个限定请求,最后使用requiresSecure表示使用HTTPS请求。这样对于Ant风格下的地址/admin/**就只能使用HTTPS协议进行请求了,而对于requiresInsecure则是取消安全请求的机制,这样就可以使用普通的HTTP请求。

3.4,防止跨站点请求伪造

跨站点请求伪造(Cross-Site Request Forgery,CSRF)是一种常见的攻击手段。首先是浏览器请求安全网站,于是可以进行登录,在登录后,浏览器会记录一些信息,以Cookie的形式进行保存,然后在不关闭浏览器的情况下,用户可能访问一个危险网站,危险网站通过获取Cookie信息来仿造用户的请求,进而请求安全网站,这样就给网站带来很大的危险。

spring aop 注册拦截器 springsecurity拦截器_自定义_09

为了克服这个危险,Spring Security提供了方案来处理CSRF过滤器。在默认的情况下,它会启用这个过滤器来防止CSRF攻击。当然,我们也可以关闭这个功能。

对于不关闭CSRF的Spring Security,每次HTTP请求的表单(Form)就要求存在CSRF参数。当访问表单的时候,Spring Security就生成CSRF参数,放入表单中,这样当提交表单到服务器时,就要求连同CSRF参数一并提交到服务器。Spring Security就会对CSRF参数进行判断,判断是否与其生成的保持一致。如果一致,它就不会认为该请求来自CSRF攻击;如果CSRF参数为空或者与服务器的不一致,它就认为这是一个来自CSRF的攻击而拒绝请求。因为这个参数不在Cookie中,所以第三方网站是无法伪造的,这样就可避免CSRF攻击。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>CSRF FORM</title>
</head>
<body>
    <form action="./commit" method="post">
        <p>
            名称:<input id="name" name="name" type="text" value="" />
        </p>
        <p>
            描述:<input id="describe" name="describe" type="text" value="" />
        </p>
        <p>
            <input type="submit" value="提交"/>
        </p>
        <input type="hidden" id="${_csrf.parameterName}"
            name="${_csrf.parameterName}" value="${_csrf.token}" />

    </form>
</body>
</html>

启用CSRF攻击的安全认证功能后,Spring Security机制就会生成对应的CSRF参数,它的属性parameterName代表的是名称,属性token代表token值。这些都会放在表单(form)的隐藏域中,所以在提交的时候会提交到服务器后端。这时Spring Security的CSRF过滤器就会去验证这个token参数是否有效,进而可以避免CSRF攻击。

4,数据库权限控制

4.1,数据库

一般项目中都是基于用户、角色、权限实现用户权限控制的,这样的设计结构简单、易于扩展。通用的用户权限模型都是基于角色的权限控制。一般情况下会有5张表,分别是用户表、角色表、权限表、用户角色关系表和角色权限对应表。每一项操作就是一个权限,只有将权限赋予某个角色,该角色下的用户才具备执行此项操作的权限。

spring aop 注册拦截器 springsecurity拦截器_spring aop 注册拦截器_10

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
  `id` bigint(20) NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES ('1', '系统管理', 'sys:system:mgr', '/system');
INSERT INTO `sys_permission` VALUES ('2', '用户管理', 'sys:user:mgr', '/user');
INSERT INTO `sys_permission` VALUES ('3', '数据管理', 'sys:data:mgr', '/data');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` bigint(20) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN');
INSERT INTO `sys_role` VALUES ('2', 'ROLE_USER');

-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
  `sys_role_id` bigint(20) NOT NULL,
  `sys_permission_id` bigint(20) NOT NULL,
  KEY `FKmnbc71b4040rgprkv4aeu0h5p` (`sys_permission_id`),
  KEY `FK31whauev046d3rg8ecubxa664` (`sys_role_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES ('1', '2');
INSERT INTO `sys_role_permission` VALUES ('1', '3');
INSERT INTO `sys_role_permission` VALUES ('1', '1');
INSERT INTO `sys_role_permission` VALUES ('2', '2');
INSERT INTO `sys_role_permission` VALUES ('2', '3');

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL,
  `password` varchar(255) DEFAULT NULL,
  `username` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_51bvuyvihefoh4kp5syh2jpi4` (`username`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'E10ADC3949BA59ABBE56E057F20F883E', 'admin');
INSERT INTO `sys_user` VALUES ('2', 'E10ADC3949BA59ABBE56E057F20F883E', 'user');

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
  `sys_user_id` bigint(20) NOT NULL,
  `sys_role_id` bigint(20) NOT NULL,
  KEY `FK1ef5794xnbirtsnudta6p32on` (`sys_role_id`),
  KEY `FKsbjvgfdwwy5rfbiag1bwh9x2b` (`sys_user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '2');

【配置数据库】修改项目中的application.properties配置文件,增加数据库连接配置。

server.port=80
server.error.path=/error
server.servlet.session.timeout=30m
server.servlet.context-path=/
server.tomcat.uri-encoding=UTF-8
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.show-sql=true

# thymeleaf
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.cache=false
spring.thymeleaf.suffix=.html

【创建实体类】创建了User、Role、Permission、RolePermisson等实体类,其中User、Role、Permission是数据库对应的实体类,RolePermisson是关联关系。

// 用户实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String username;
    private String password;
    private List<Role> roles;
}// 角色实体类
@Data
@NoArgsConstructor
public class Role implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String name;
    private List<Permission> permissions;

    public Role(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}// 权限实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Permission implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long id;
    private Long pid;
    private String name;
    private String url;
    private String description;
}// 角色权限实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RolePermisson implements Serializable {
    private Long roleId;
    private String roleName;
    private Long permissionId;
    private String url;
}

【创建数据库操作类】

@Mapper
public interface SysUserRepository {

    /**
     * 通过username查找 user
     * username是唯一的前提
     */
    @Select("select id,password,username from sys_user WHERE username=#{username}")
    SysUser findUserByUsername(String username);

    /**
     * 通过用户名 查找·
     */
    @Select("select sr.id,sr.name from sys_user as su join sys_role as sr where su.username=#{username}")
    List<SysRole> findRolesByUsername(String username);

    /**
     * 通过用户名 查找权限
     */
    @Select("select sp.* from sys_user su left join sys_user_role  sur on su.id = sur.sys_user_id " +
            "left join sys_role_permission srp on sur.sys_role_id = srp.sys_role_id" +
            "left join sys_permission sp on srp.sys_permission_id = sp.id" +
            "where su.username =#{username}")
    List<SysPermission> findPermissionsByUsername(String username);

    @Select("select sr.id,sr.name,sp.id,sp.url from sys_role as sr join sys_permission as sp")
    List<SysRolePermisson> findAllRolePermissoin();
}

4.2,Security配置

Security的配置比较复杂,需要自定义UserDetailsService和AccessDecisionManager,实现AbstractSecurityInterceptor拦截器,还要自定义SecurityMetadataSource。最后,重写WebSecurityConfigurerAdapter将上面的自定义配置加入Security过滤链中。

【添加依赖】

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.10</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.nudt</groupId>
	<artifactId>Security</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>Security</name>
	<description>Security</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.10</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.29</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>
	</dependencies>

	<build>
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<includes>
					<include>**/*.xml</include>
				</includes>
			</resource>
			<resource>
				<directory>src/main/resources</directory>
			</resource>
		</resources>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

【UserDetailsService】先实现自定义的UserDetailsService,再实现自定义的loadUserByUsername用户数据加载功能。

@Component
public class MyCustomUserService implements UserDetailsService {
    private final static Logger logger = LoggerFactory.getLogger(MyCustomUserService.class);
    @Autowired
    private SysUserRepository sysUserRepository;

    /**
     * 通过验证 将用的所有角色 用户信息中
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        logger.info("根据名称获取用户信息: username is {}", username);

        SysUser user = sysUserRepository.findUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
        }

        //获取所有请求的url
        //List<SysPermission> sysPermissions = sysUserMapper.findPermissionsByUsername(user.getUsername());
        List<SysRole> sysRoles = sysUserRepository.findRolesByUsername(user.getUsername());

        logger.info("用户角色个数为{}", sysRoles.size());
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        for (SysRole sysRole : sysRoles) {
            //封装用户信息和角色信息 到 SecurityContextHolder全局缓存中
            logger.info("name--->{}", sysRole.getName());
            grantedAuthorities.add(new SimpleGrantedAuthority(sysRole.getName()));
        }
        return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
    }
}

【自定义AccessDecisionManager】Spring Security通过AccessDecisionManager来决定对于一个用户的请求是否基于通过的中心控制。首先定义MyAccessDecisionManager类并继承AccessDecisionManager接口。

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {

    private final static Logger logger = LoggerFactory.getLogger(MyAccessDecisionManager.class);

    /**
     * 判定 是否含有权限
     */
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        if(null== configAttributes || configAttributes.size() <=0) {
            return;
        }
        String needRole;
        for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
            needRole = iter.next().getAttribute();

            for(GrantedAuthority ga : authentication.getAuthorities()) {
                if(needRole.trim().equals(ga.getAuthority().trim())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("no privilege");
    }

    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    public boolean supports(Class<?> clazz) {
        return true;
    }
}

最重要的是decide()方法,通过用户的登录信息验证是否有权限执行当前的HTTP请求。首先,Security会将当前登录用户信息包装到一个Authentication对象中,并且调用getAttributes()方法获取这个URL相关的权限,以参数Collection<ConfigAttribute>的形式传入这个方法。然后,decide()方法获取信息之后进行对比决策,如果当前用户允许登录,那么直接return即可;如果当前用户不允许登录,则抛出一个AccessDeniedException异常。

【实现自定义的AbstractSecurityInterceptor】Spring Security是通过过滤器发挥作用的,需要将决策管理器与MyCustomUserService数据加载器放到过滤器中,然后将这个过滤器插入Security的过滤器链。

@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }


    public void invoke(FilterInvocation fi) throws IOException, ServletException {

        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            //执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    public void destroy() {

    }

    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

【自定义SecurityMetadataSource】Spring Security中的SecurityMetadataSource用于加载URL与权限的对应关系,对于这个我们需要自己进行定义。

@Component
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private SysUserRepository sysUserRepository;

    /**
     * 每一个资源所需要的角色
     */
    private static HashMap<String, Collection<ConfigAttribute>> map = null;

    public void loadResourceDefine() {

        map = new HashMap<>();

        //权限资源 和 角色对应的表  也就是 角色 权限中间表
        List<SysRolePermisson> rolePermissons = sysUserRepository.findAllRolePermissoin();
        //每个资源 所需要的权限
        for (SysRolePermisson rolePermisson : rolePermissons) {
            String url = rolePermisson.getUrl();
            String roleName = rolePermisson.getRoleName();
            ConfigAttribute role = new SecurityConfig(roleName);
            if (map.containsKey(url)) {
                map.get(url).add(role);
            } else {
                map.put(url, new ArrayList<ConfigAttribute>() {{
                    add(role);
                }});
            }
        }
    }

    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        if (map == null) {
            loadResourceDefine();
        }
        //object 中包含用户请求的request 信息
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        for (Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
            String url = iter.next();
            if (new AntPathRequestMatcher(url).matches(request)) {
                return map.get(url);
            }
        }
        return null;
    }

    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    public boolean supports(Class<?> clazz) {
        return true;
    }
}

【重写WebSecurityConfigurerAdapter】

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 通过 实现UserDetailService 来进行验证
     */
    @Autowired
    private MyCustomUserService myCustomUserService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //校验用户
        //校验密码
        auth.userDetailsService(myCustomUserService).passwordEncoder(new PasswordEncoder() {

            public String encode(CharSequence rawPassword) {
                return Md5Util.MD5(String.valueOf(rawPassword));
            }

            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(Md5Util.MD5(String.valueOf(rawPassword)));
            }
        });
    }

    /**
     * 创建自定义的表单
     * 页面、登录请求、跳转页面等
     */
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "index", "/login", "/css/**", "/js/**")//允许访问
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")//拦截后get请求跳转的页面
                .defaultSuccessUrl("/content")
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }
}

【创建Controller】

@Controller
public class TestController {

    @RequestMapping("/")
    public String index() {
        return "/index.html";
    }

    @RequestMapping("/login")
    public String login() {
        return "/login.html";
    }

    @RequestMapping("/content")
    public String content() {
        return "/content/content.html";
    }

    @RequestMapping("/admin")
    public String admin() {
        return "/admin/admin.html";
    }
}