由于Spring Security内容较多,本人决定先学Spring Security然后继续更新springboot

1.认识Spring Security

  Spring Security提供了声明式的安全访问控制解决方案(仅支持基于Spring的应用程序),对访问权限进行认证和授权,它基于Spring AOP和Servlet过滤器,提供了安全性方面的全面解决方案。

  除常规的认证和授权外,它还提供了 ACLs、LDAP、JAAS、CAS等高级特性以满足复杂环境下的安全需求。

  1.1 核心概念

  Spring Security的3个核心概念。

  • Principle:代表用户的对象Principle ( User),不仅指人类,还包括一切可以用于验证的设备。
  • Authority:  代表用户的角色Authority ( Role ),每个用户都应该有一种角色,如管理员或是会员。
  • Permission:代表授权,复杂的应用环境需要对角色的权限进行表述。

  在Spring Security中,Authority和Permission是两个完全独立的概念,两者并没有必然的联系。它们之间需要通过配置进行关联,可以是自己定义的各种关系。

  1.2 认证和授权

  安全主要分为验证(authentication)和授权(authorization )两个部分。

   (1)验证(authentication) 

  验证指的是,建立系统使用者信息(Principal)的过程。使用者可以是一个用户、设备,和可以在应用程序中执行某种操作的其他系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码的正确性来完成认证的通过或拒绝过程。Spring Security支持主流的认证方式,包括HTTP基本认证、HTTP表单验证、HTTP摘要认证、OpenlD和LDAP等。

  Spring Security进行验证的步骤如下:

  1. 用户使用用户名和密码登录。
  2. 过滤器(UsernamePasswordAuthenticationFilter)获取到用户名、密码,然后封装成 Authentication。
  3. AuthenticationManager 认证 token ( Authentication 的实现类传递)。
  4. AuthenticationManager认证成功,返回一个封装了用户权限信息的Authentication对象, 用户的上下文信息(角色列表等)。
  5. Authentication对象赋值给当前的SecurityContext,建立这个用户的安全上下文(通过调用 getContext().setAuthentication())。
  6. 用户进行一些受到访问控制机制保护的操作,访问控制机制会依据当前安全上下文信息检查这个操作所需的权限。

  除利用提供的认证外,还可以编写自己的Filter(过滤器),提供与那些不是基于Spring Security 的验证系统的操作。

  (2)授权(authorization)。

  在一个系统中,不同用户具有的权限是不同的。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。它判断某个Principal在应用程序中是否允许执行某个操作。在进行授权判断之前,要求其所要使用到的规则必须在验证过程中已经建立好了。对Web资源的保护,最好的办法是使用过滤器。对方法调用的保护,最好的办法是使用AOP。Spring Security在进行用户认证及授予权限时,也是通过各种拦截器和AOP来控制权限访问的,从而实现安全。

  1.3 模块

  • 核心模块——spring-security-core.jar:包含核心验证和访问控制类和接口,以及支持远程配置的基本API。
  • 远程调用——spring-security-remoting.jar:提供与 Spring Remoting 集成。
  • 网页——spring-security-web.jar:包括网站安全的模块,提供网站认证服务和基于URL 访问控制。
  • 配置——spring-security-config.jar:包含安全命令空间解析代码。
  • LDAP——spring-security-ldap.jar: LDAP 验证和配置。
  • ACL——spring-security-acl.jar:对 ACL 访问控制表的实现。
  • CAS——spring-security-cas.jar:对 CAS 客户端的安全实现。
  • OpenlD——spring-security-openid.jar:对 OpenlD 网页验证的支持。
  • Test——spring-security-test.jar:对 Spring Security 的测试的支持。

2. 核心类

  2.1 SecurityContext

  Securitycontext中包含当前正在访问系统的用户的详细信息,它只有以下两种方法。

  • getAuthentication():获取当前经过身份验证的主体或身份验证的请求令牌。
  • setAuthentication():更改或删除当前已验证的主体身份验证信息。

  SecurityContext 的信息是由 SecurityContextHolder 来处理的。

  2.2 SecurityContextHolder

  SecurityContextHolder 用来保存 SecurityContext。最常用的是 getContext()方法,用来获得当前 SecurityContext。

  SecurityContextHolder中定义了一系列的静态方法,而这些静态方法的内部逻辑是通过 SecurityContextHolder 持有的 SecurityContextHolderStrategy 来实现的,如 clearContext()、 getContext ()、setContext、createEmptyContext()。SecurityContextHolderStrategy 接口的关键代码如下:

查看代码

 /*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.core.context;

/**
 * A strategy for storing security context information against a thread.
 *
 * <p>
 * The preferred strategy is loaded by {@link SecurityContextHolder}.
 *
 * @author Ben Alex
 */
public interface SecurityContextHolderStrategy {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Clears the current context.
	 */
	void clearContext();

	/**
	 * Obtains the current context.
	 *
	 * @return a context (never <code>null</code> - create a default implementation if
	 * necessary)
	 */
	SecurityContext getContext();

	/**
	 * Sets the current context.
	 *
	 * @param context to the new argument (should never be <code>null</code>, although
	 * implementations must check if <code>null</code> has been passed and throw an
	 * <code>IllegalArgumentException</code> in such cases)
	 */
	void setContext(SecurityContext context);

	/**
	 * Creates a new, empty context implementation, for use by
	 * <tt>SecurityContextRepository</tt> implementations, when creating a new context for
	 * the first time.
	 *
	 * @return the empty context.
	 */
	SecurityContext createEmptyContext();
}

  (1)strategy 实现

  默认使用的 strategy 就是基于 ThreadLocal 的 ThreadLocalSecurityContextHolderStralegy 来实现的。

  除了上述提到的,Spring Security还提供了 3种类型的strategy来实现。

  • GlobalSecurityContextHolderStrategy:表示全局使用同一个 SecuntyContext,如 C/S 结构的客户端。
  • InheritableThreadLocalSecuntyContextHolderStrategy:使用 InhentableThreadLocal 来存放Security Context, 即子线程可以使用父线程中存放的变量。
  • ThreadLocalSecuntyContextHolderStrategy: 使用ThreadLocal 来存放 SecurityContext

  —般情况下,使用默认的strategy即可。但是,如果要改变默认的strategy, Spring Security 提供了两种方法来改变”strategyName”

  SecuntyContextHolder 类中有 3 种不同类型的 strategy,分别为 MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL和MODE_GLOBAL,关键代码如下:

    public static final String MODE_THREADLOCAL = “MODE_THREADLOCAL”;

    publrc static final String MODE_INHERITABLETHREADLOCAL = “MODE_INHERITABLETHREADLOCAL”;

    public static final String MODE_GLOBAL = “MODE_GLOBAL”;

    public static final String SYSTEM_PROPERTY = “spring.security.strategy”;

    private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

    private static SecurityContextHolderStrategy strategy;

  MODE_THREADLOCAL是默认的方法。

  如果要改变strategy,则有下面两种方法:

  • 通过 SecurityContextHolder 的静态方法 setStrategyName(java.Iang.String.strategyName) 来改变需要使用的strategy
  • 通过系统属性(SYSTEM_PROPERTY )进行指定,其中属性名默认为”spring.security.strategy”,属性值为对应strategy的名称。

  (2)获取当前用户的SecurityContext()

  Spring Security使用一个Authentication对象来描述当前用户的相关信息。Security-ContextHolder中持有的是当前用户的SecurityContext,而SecurityContext持有的是代表当前用户相关信息的Authentication的引用。

  这个Authentication对象不需要自己创建,Spring Security会自动创建相应的Authentication 对象,然后赋值给当前的SecurityContext。但是,往往需要在程序中获取当前用户的相关信息, 比如最常见的是获取当前登录用户的用户名。在程序的任何地方,可以通过如下方式获取到当前用户的用户名。

public String getCurrentUsername(){
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if (principal instanceof UserDetails){
        return ((UserDetails) principal).getUsername();
    }
    if (principal instanceof Principal){
        return ((Principal) principal).getName();
    }
    return String.valueOf(principal);
}

  getAuthentication()方法会返回认证信息。

  getPrincipal()方法返回身份信息,它是UserDetails对身份信息的封装。

  获取当前用户的用户名,最简单的方式如下:

public String getCurrentUsername(){
    return SecurityContextHolder.getContext().getAuthentication().getName();
}

  在调用 SecurityContextHolder.getContext()获取 SecurityContext 时,如果对应的 Securitycontext 不存在,则返回空的 SecurityContext。

  2.3 ProviderManager

  ProviderManager会维护一个认证的列表,以便处理不同认证方式的认证,因为系统可能会存在多种认证方式,比如手机号、用户名密码、邮箱方式。

  在认证时,如果ProviderManager的认证结果不是null,则说明认证成功,不再进行其他方式的认证,并且作为认证的结果保存在SecurityContext中。如果不成功,则抛出错误信息 “ProviderNotFoundException”

  2.4 DaoAuthenticationProvider

  它是AuthenticationProvider最常用的实现,用来获取用户提交的用户名和密码,并进行正确性比对。如果正确,则返回一个数据库中的用户信息。

  当用户在前台提交了用户名和密码后,就会被封装成UsernamePasswordAuthentication-Token。然后,DaoAuthenticationProvider 根据 retrieveUser方法,交给 additionalAuthentication- Checks方法完成 UsernamePasswordAuthenticationToken 和 UserDetails 密码的比对。如果这个方法没有抛出异常,则认为比对成功。

  比对密码需要用到PasswordEncoder和SaltSource。

  2.5 UserDetails

  UserDetails是Spring Security的用户实体类,包含用户名、密码、权限等信息。Spring Security默认实现了内置的User类,供Spring Security安全认证使用,当然,也可以自己实现。

    UserDetails 接口和 Authentication 接口很类似,都拥有 username 和 authorities。一定要区分清楚Authentication 的 getCredentials()与 UserDetails 中的 getPassword()。前者是用户提交的密码凭证,不一定是正确的,或数据库不一定存在;后者是用户正确的密码,认证器要进行比对的就是两者是否相同。

  Authentication 中的 getAuthorities()方法是由 UserDetails 的 getAuthorities()传递而形成 的。UserDetails的用户信息是经过Authenticationprovider认证之后被填充的

  UserDetails中提供了以下几种方法。

  • String getPassword():返回验证用户密码,无法返回则显示为null。
  • String getUsemame():返回验证用户名,无法返回则显示为nulL
  • boolean isAccountNonExpired():账户是否过期:过期无法验证。
  • boolean isAccountNonLocked():指定用户是否被锁定或解锁,锁定的用户无法进行身份验证。
  • boolean isCredentialsNonExpired():指定是否已过期的用户的凭据(密码),过期的凭据无法认证。
  • boolean isEnabled():是否被禁用。禁用的用户不能进行身份验证。

    2.6 UserDetailsService

  用户相关的信息是通过UserDetailsService接口来加载的。该接口的唯一方法是 loadUserByUsername(String username),用来根据用户名加载相关信息。这个方法的返回值是 UserDetails接口,其中包含了用户的信息,包括用户名、密码、权限、是否启用、是否被锁定、 是否过期等。

  2.7 GrantedAuthority

  GrantedAuthonty中只定义了一个getAuthority()方法。该方法返回一个字符串,表示对应权限的字符串。如果对应权限不能用字符串表示,则返回nulL

  GrantedAuthority 接口通过 UserDetailsService 进行加载,然后赋予 UserDetails。

  Authentication的getAuthorities()方法可以返回当前Authentication对象拥有的权限,其返回值是一个GrantedAuthority类型的数组。每一个GrantedAuthority对象代表赋予当前用户的一 种权限。

  2.8 Filter

    (1)SecurityContextPersistenceFilter

  它从SecurityContextRepository中取出用户认证信息。为了提高效率,避免每次请求都要查询认证信息,它会从Session中取岀已认证的用户信息,然后将其放入SecurityContextHolder 中,以便其他Filter使用。

  (2)WebAsyncManagerlntegrationFilter

  集成了 SecurityContext 和 WebAsyncManager,把 Securitycontext 设置到异步线程,使其也能获取到用户上下文认证信息。

   (3)HanderWriterFilter

  它对请求的Header添加相应的信息。

    (4)CsrfFilter

  跨域请求伪造过滤器。通过客户端传过来的token与服务器端存储的token进行对比,来判断请求的合法性。

  (5)LogoutFilter

  匹配登岀URL。匹配成功后,退出用户,并清除认证信息。

  (6)UsernamePasswordAuthenticationFilter

  登录认证过滤器,默认是对“/login”的POST请求进行认证。该方法会调用attemptAuthentication, 尝试获取一个Authentication认证对象,以保存认证信息,然后转向下一个Filter,最后调用 successfulAuthenlication 执行认证后的事件。

  (7)AnonymousAuthenticationFilter

  如果SecurityContextHolder中的认证信息为空,则会创建一个匿名用户到Security-ContextHolder 中

  (8)SessionManagementFilter

  持久化登录的用户信息。用户信息会被保存到Session、Cookie、或Redis中。

3.配置Spring Security

3.1 继承 WebSecurityConfigurerAdapter

  通过重写抽象接口 WebSecurityConfigurerAdapter,再加上注解@EnableWebSecurity, 可以实现Web的安全配置。

    WebSecurityConfigurerAdapter Config 模块一共有 3 个 builder (构造程序)。

  • AuthenticationManagerBuilder:认证相关builder,用来配置全局的认证相关的信息。它包含AuthenticationProvider和UserDetailsService f前者是认证服务提供者,后者是用户详情查询服务。
  • HttpSecurity:进行权限控制规则相关配置。
  • WebSecurity:进行全局请求忽略规则配置、HttpFirewall配置、debug配置、全局 SecurityFilterChain 配置。

  配置安全,通常要重写以下方法:

//通过auth对象的方法添加身份验证
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception{}
//通常用于设置忽略权限的静态资源
public void configure(WebSecurity webSecurity) throws Exception{}
//通过HTTP对象的authorizeRequests()方法定义URL访问权限。默认为formLogin()提供一个简单的登录验证页面
protected void configure(HttpSecurity httpSecurity) throws Exception{}

3.2 配置自定义策略

  配置安全需要继承WebSecurityConfigurerAdapter,然后重写其方法,见以下代码:

package com.intehel.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
//指定为配置类
@EnableWebSecurity
//指定为 Spring Security	如果是 WebFlux,则需要启用@EnableWebFluxSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
//如果要启用方法安全设置,则开启此项。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        //不拦截静态资源
        web.ignoring().antMatchers("/static/**");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        //使用BCrypt加密
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().usernameParameter("uname").passwordParameter("pwd").loginPage("admin/login").permitAll()
                .and().authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")
                //除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        http.logout().permitAll();
        http.rememberMe().rememberMeParameter("rememberMe");
        //处理异常,拒绝访问就重定向到403页面
        http.exceptionHandling().accessDeniedPage("/403");
        http.logout().logoutSuccessUrl("/");
        http.csrf().ignoringAntMatchers("/admin/upload");
    }
}

代码解释如下。

  • authorizeRequests():  定义哪些URL需要被保护,哪些不需要被保护。
  • antMatchers(“/admin/**”).hasRole(“ADMIN”),定义/admin/下的所有 URL。只有拥有 admin角色的用户才有访问权限。
  • formLogin():自定义用户登录验证的页面。
  • http.csrfO:配置是否开JSCSRF保护,还可以在开启之后指定忽略的接口。
<!DOCTYPE html>
<html lang="en"
      xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/tyemeleaf-extras-springsecurity5">
<head>
    <!--如果开启了 CSRF,则一定在验证页面加入以下代码以传递token值:-->
    <meta name="_csrf" th:content="${_csrf.token}">
    <meta name="_csrf_header" th:content="${_csrf.headerName}">
</head>
<body>
    <form>
        <!--如果要提交表单,则需要在表单中添加以下代码以提交token值-->
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
        <!-- http.rememberMe(): "记住我"功能,可以指定参数。使用时,添加如下代码:-->
        <input class="i-checks" type="checkbox" name="rememberme"/>&nbsp;&nbsp;记住我
    </form>
</body>
</html>

3.3 配置加密方式

@Bean
public PasswordEncoder passwordEncoder() {
    //使用BCrypt加密
    return new BCryptPasswordEncoder();
}

在业务代码中,可以用以下方式对密码进行加密:

BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encryptedPassword = bCryptPasswordEncoder.encode(password);

3.4 自定义加密规则

  除默认的加密规则,还可以自定义加密规则。具体见以下代码:

protected void encode(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(UserService()).passwordEncoder(new PasswordEncoder(){
        @Override
        public String encode(CharSequence rawPassword) {
            return MD5Util.encode((String)rawPassword);
        }
        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return encodedPassword.equals(MD5Util.encode((String)rawPassword));
        }
    })
}

3.5 配置多用户系统

  一个完整的系统一般包含多种用户系统,比如”后台管理系统+前端用户系统”。Spring Security 默认只提供一个用户系统,所以,需要通过配置以实现多用户系统。

  比如,如果要构建一个前台会员系统,则可以通过以下步骤来实现。

  (1)构建UserDetailsService用户信息服务接口

package com.intehel.service;

import com.intehel.domain.User;
import com.intehel.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public class UserSecurityService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByName(username);
        if (user==null){
            User mobileUser = userRepository.findByMobile(username);
            if (mobileUser==null){
                User emailUser = userRepository.findByEmail(username);
                if (emailUser==null){
                    throw new UsernameNotFoundException("用户名,邮箱或手机号不存在!");
                }else {
                    user = userRepository.findByEmail(username);
                }
            }else {
                user = userRepository.findByMobile(username);
            }
        }else if ("locked".equals(user.getStatus())){
            throw new LockedException("用户被锁定");
        }
        return user;
    }
}

   (2)进行安全配置

  在继承 WebSecurityConfigurerAdapter 的 Spring Security 配置类中,配置 UserSecurity- Service 类。

@Bean
UserDetailsService UserService() {
    return new UserSecurityService();
}

  如果要加入后台管理系统,则只需要重复上面步骤即可。

3.6 获取当前登录用户信息的几种方式

  获取当前登录用户的信息,在权限开发过程中经常会遇到。而对新人来说,不太了解怎么获取, 经常遇到获取不到或报错的问题。所以,本节讲解如何在常用地方获取当前用户信息。

  (1)在Thymeleaf视图中获取

  要Thymeleaf视图中获取用户信息,可以使用Spring Security的标签特性。

  在Thymeleaf页面中引入Thymeleaf的Spring Security依赖,见以下代码:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div sec:authorize="isAnonymous()">
        未登录,单击<a th:href="@{/home/login}">登录</a>
    </div>
    <div sec:authorize="isAuthenticated()">
        <p>已登录</p>
        <p>登录名:<span sec:authentication="name"></span></p>
        <p>角色:<span sec:authentication="principal.authorities"></span></p>
        <p>name:<span sec:authentication="principal.username"></span></p>
        <p>password:<span sec:authentication="principal.password"></span></p>
    </div>
</body>
</html>

  这里要特别注意版本的对应。如果引入了 thymeleaf-extras-springsecurity依赖依然获取不到信息,那么可能是Thymeleaf版本和thymeleaf-extras-springsecurity的版本不对,请检查在pom.xrnl文件的两个依赖,见以下代码,springboot中需加入starter依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.0.7.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.1.0.M1</version>
</dependency>

  (2)在Controller中获取

  在控制器中获取用户信息有3种方式

@GetMapping("userinfo")
public String getProduct(Principal principal, Authentication authentication,HttpServletRequest request){
    /**
     * 1.通过Principal获取
     *  */
    String username1 = principal.getName();
    /**
     * 2.通过Authentication获取
     *  */
    String username2 = authentication.getName();
    /**
     * 3.通过HttpServletRequest获取
     *  */
    Principal httpPrincipal = request.getUserPrincipal();
    String username3 = httpPrincipal.getName();
    return username1;
}

  (3)在Bean中获取

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof AnonymousAuthenticationToken)){
    String username = authentication.getName();
    return username;
}

  在其他 Authentication 类也可以这样获取。比如在 UsemamePasswoEAuthenticationToken 类中。

  如果上面代吗获取不到,并不是代码错误,则可能是因为以下原因造成的

  1. 要使上面的获取生效,必须在继承 WebSecurityConfigurerAdapter的类中的http.antMatcher(“/*”)的鉴权 URI 范围内。
  2. 没有添加 Thymeleaf 的 thymeleaf-extras-springsecurity 依赖。
  3. 添加了 Spring Security 的依械,但是版本不对,比如 Spring Security 和 Thymeleaf 的版本不对。

3.7 用Spring Security来实现后台登录及权限认证功能

  (1)引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.0.7.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.1.0.M1</version>
</dependency>

  (2)创建权限开放的页面

  这个页面是不需要鉴权即可访问的,以区别演示需要鉴权的页面,见以下代码:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>security案例</title>
</head>
<body>
    <h1>welcome</h1>
    <p><a th:href="@{/home}">会员中心</a></p>
</body>
</html>

  (3)创建需要权限验证的页面

  其实可以和不需要鉴权的页面一样,鉴权可以不在HTML页面中进行,见以下代码:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <title>home</title>
</head>
<body>
    <h1>hello 会员中心</h1>
    <p th:inline="text">hello<span sec:authentication="name"></span></p>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="登出">
    </form>
</body>
</html>

  使用 Spring Security 5 之后,可以在模板中用<span sec:authentication=”name”></span> 或[[${#httpServletRequest.remoteUser}]]来获取用户名。登岀请求将被发送到“/logout”。成功 注销后,会将用户重定向到”/login?logout”

  (4)配置 Spring Security

  1.配置 Spring MVC

  可以继承WebMvcConfigurer,具体使用见以下代码:

package com.intehel.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("authorize_security");
        registry.addViewController("/").setViewName("open_security");
        registry.addViewController("/login").setViewName("login");
    }
}

  2.配置 Spring Security

  Spring Security的安全配置需要继承WebSecurityConfigurerAdapter,然后重写其方法, 见以下代码:

package com.intehel.config;

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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
//指定为配置类
@EnableWebSecurity
//指定为 Spring Security	如果是 WebFlux,则需要启用@EnableWebFluxSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
//如果要启用方法安全设置,则开启此项。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/","/welcome","/login").permitAll()
                //除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login").defaultSuccessUrl("/home")
                .and()
                .logout().permitAll();
    }
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String password = bCryptPasswordEncoder.encode("123");
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin").password(password)
                .roles("USER");
    }
}

代码解释如下

  • @EnableWebSecurity 注解:集成了 Spmg Security 的 Web 安全支持。
  • @WebSecurityConfig:在配置类的同时集成了 WebSecurityConfigurerAdapter,重写了其中的特定方法,用于自定义Spring Security配置。Spring Security的工作量都集中在该配置类。
  • configure(HttpSecurity):定义了哪些URL路径应该被拦截。
  • configureGlobal(AuthenticationManagerBuilder):任内存中配置一个用户, admin/123这个用户拥有User角色。

  3.创建登录页面

  登录页面要特别注意是否开启了 CSRF功能。如果开启了,则需要提交token信息。创建的登录页面见以下代码:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>security example</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>

  测试权限:

  (1)启动项目,访问首页”http://localhost:8080″,单击“会员中心”,尝试访问受限的页面 “http://localhost:8080/home”由于未登录,结果被强制跳转到登录页面”http://localhost: 8080/login”

  (2)输入正确的用户名和密码(admin、123)之后,跳转到之前想要访问的“/home:”, 显示用户名admin。

  (3)单击“登出”按钮,回到登录页面。

3.8 权限控制方式

  (1)Spring EL权限表达式

  Spring Security支持在定义URL访问或方法访问权限时使用Spring EL表达式。根据表达式返回的值(true或false)来授权或拒绝对应的权限。Spring Security可用表达式对象的基类是 SecurityExpressionRoot,它提供了通用的内置表达式,见下表。

  

  

  在视图模扳文件中,可以通过表达式控制显示权限,如以下代码:

<p sec:authorize="hasRole('ROLE_ADMIN')">管理员</p>
<p sec:authorize="hasRole('ROLE_USER')">管理员</p>

  在WebSecurityConfig中添加两个内存用户用于测试,角色分别是ADMIN、USER:

.withUser("admin").password("123456").roles("ADMIN")
.and().withUser("user").password("123456").roles("USER");

  用户admin登录,则显示:

  管理员

  用户user登录,则显示:

  普適用户

  然后,在WebSecurityConfig中加入如下的URL权限配置:

  .antMatchers("/home").hasRole("ADMIN")

  这时,当用admin用户访问“home”页面时能正常访问,而用user用户访问时则会提示“403 禁止访问”。因为,这段代码配置使这个页面访问必须具备ADMIN (管理员)角色,这就是通过 URL控制权限的方法。

  (2)通过表达式控制URL权限

  如果要限定某类用户访问某个URL,则可以通过Spring Security提供的基于URL的权限控制来实现。Spring Security 提供的保护URL 的方法是重写configure(HttpSecurity http)方法, HttpSecurity提供的方法见下表

还需要额外补充以下几点。

  • authenticated:保护URL,需要用户登录。如:anyRequest().authenticated()代表其他未配置的页面都已经授权。
  • permitAII():  指定某些URL不进行保护。一般针对静态资源文件和注册等未授权情况下需要访问的页面。
  • hasRole(String role):限制单个角色访问。在Spring Security中,角色是被默认增加 “ROLE_”前缀的,所以角色”ADMIN”代表”ROLE_ADMIN”。
  • hasAnyRole(String- roles):允许多个角色访问。这和Spring Boot1. x版本有所不同。
  • access(String attribute):该方法可以创建复杂的限制,比如可以增加RBAC的权限表达式。
  • haslpAddress(String ipaddressExpression):用于限制 IP 地址或子网。

具体用法见以下代码:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/static","/register").permitAll()
            .antMatchers("/user/**").hasAnyRole("USER","ADMIN")
            .antMatchers("/admin/**").access("hasRole('ADMIN') and hasIpAddress('localhost')")
            .anyRequest().authenticated();
}

  (3)通过表达式控制方法权限

  要想在方法上使用权限控制,则需要使用启用方法安全设置的注解@EnableGlobalMethodSecurity()它默认是禁用的,需要在继承WebSecurityConfigurerAdapter的类上加注解来启用, 还需要配置启用的类型,它支持开启如下三种类型。

  • @EnableGlobalMethodSecurity(jsr250Enabled = true):开启 JSR-250,
  • @EnableGlobalMethodSecurity(prePostEnabled = true):开启 prePostEnabled。
  • @EnableGlobalMethodSecurity(securedEnabled= true):开启 secured。

  1.JSR-250

  JSR是Java Specification Requests的缩写,是Java规范提案。任何人都可以提交JSR, 以向Java平台増添新的API和服务。JSR是Java的一个重要标准。

  Java 提供了很多 JSR,比如 JSR-250、JSR-303、JSR-305、JSR-308。初学者可能会 对JSR有疑惑。大家只需要记住“不同的JSR其功能定义是不一样的”即可。比如:JSR-303 主要是为数据的验证提供了一些规范的API。这里的JSR-250是用于提供方法安全设置的,它主要提供了注解 @RolesAllowed。

  它提供的方法主要有如下几种。

  • @DenyAII:拒绝所有访问°
  • @RolesAllowed({“USER”,”ADMIN”}):该方法只要具有“USER”、“ADMIN”任意一种权限就可以访问。
  • @PermitAII:  允许所有访问。

  2.prePostEnabled

  除JSR-250注解外,还有prePostEnabled 它也是基于表达式的注解,并可以通过继承GlobalMethodSecurityConfiguration类来实现自定义功能。如果没有访问方法的权限,则会抛出 AccessDeniedException.

  它主要提供以下4种功能注解。

  (1)@PreAuthorize

  它在方法执行之前执行,使用方法如下:

  a.限制userid的值是否等于principal中保存的当前用户的userid,或当前用户是否具有 ROLE_ADMIN 权限。

    @PreAuthorize(“#userld == authentication.principal.userid or hasAuthority(‘ADMIN’)”)

  b.限制拥有ADMIN角色才能执行。

    @PreAuthorize(“hasRole(‘ROLE_ADMIN’)”)

  c.限制拥有ADMIN角色或USER角色才能执行。

    @PreAuthorize(“hasRole(‘ROLE_USER’) or hasRole(‘ROLE_ADMIN’)”)

  d.限制只能査询id小于3的用户才能执行。

    @PreAuthorize(“#id<3”)

  e.限制只能查询自己的信息,这里一定要在当前页面经过权限验证,否则会报错。

    @PreAuthorize(“principal.username.equals(#username)”)

  f.限制用户名只能为long的用户。

    @PreAuthorize(“#user.name.equals(‘long’)”)

  对于低版本的Spring Security,添加注解之后还需要将AuthenticationManager定义为 Bean,具体见以下代码:

@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
}
  (2)@PostAuthorize

  表示在方法执行之后执行,有时需要在方法调用完后才进行权限检查。可以通过注解 @PostAuthorize 达到这一效果。

  注解@PostAuthorize是在方法调用完成后进行权限检查的,它不能控制方法是否能被调用, 只能在方法调用完成后检查权限,来决定是否要拋岀AccessDeniedException,这里也可以调用方法的返回值。如果EL为false,那么该方法己经执行完了,可能会回滚。EL 变量returnObject表示返回的对象,如:

  @PostAuthorize("returnObject.userId==authentication.principal.userId or hasPermission(returnObject,'ADMIN')")

  (3)@PreFilter

  表示在方法执行之前执行。它可以调用方法的参数,然后对参数值进行过滤、处理或修改。EL 变量filterObject表示参数。如有多个参数,则使用filterTarget注解参数。方法参数必须是集合或数组

   (4)@postFilter

  表示在方法执行之后执行。而且可以调用方法的返回值,然后对返回值进行过滤、处理或修改, 并返回。EL变量returnObject表示返回的对象。方法霊要返回集会或数组。

  如使用@PreFilter和@PostFilter时,Spring Security将移除使对应表达式的结果为false 的元素。

  当Filter标注的方法拥有多个集合类型的参数时,需要通过filterTarget属性指定当前是针对哪个参数进行过滤的。

  4. securedEnabled

  开启securedEnabled支持后,可以使用注解@Secured来认证用户是否有权限访问。使用方法见以下代码:

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")public User getUser(Long userId);
@Secured("ROLE_TELLER")

  实例:使用JSR-250注解

    (1)开启支持

    在安全配置类中,启用@EnableGlobalMethodSecurity(jsr250Enabled = true)

    (2)创建user服务接口 UserService,见以下代码:

package com.intehel.service;

public interface UserService {
    public String addUser();
    public String updateUser();
    public String deleteUser();
}

    (3)实现user服务接口的方法,见以下代码:

package com.intehel.service.Impl;

import com.intehel.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.security.RolesAllowed;

@Service
public class UserServiceImpl implements UserService {
    @Override
    public String addUser() {
        System.out.println("addUser");
        return null;
    }

    @Override
    @RolesAllowed({"ROLE_USER","ROLE_ADMIN"})
    public String updateUser() {
        System.out.println("updateUser");
        return null;
    }

    @Override
    @RolesAllowed({"ROLE_ADMIN"})
    public String deleteUser() {
        System.out.println("deleteUser");
        return null;
    }
}

    (4)编写控制器,见以下代码:

package com.intehel.controller;

import com.intehel.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("/addUser")
    public void addUser(){
        userService.addUser();
    }
    @GetMapping("/updateUser")
    public void updateUser(){
        userService.updateUser();
    }
    @GetMapping("/deleteUser")
    public void deleteUser(){
        userService.deleteUser();
    }
}

    (5)配置类

package com.intehel.config;

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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
//指定为配置类
@EnableWebSecurity
//指定为 Spring Security	如果是 WebFlux,则需要启用@EnableWebFluxSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .antMatchers("/","/welcome","/login").permitAll()
                .antMatchers("/home").hasRole("ADMIN")
                //除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login").defaultSuccessUrl("/home")
                .and()
                .logout().permitAll();
    }
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String password = bCryptPasswordEncoder.encode("123");
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin").password(password)
                .roles("ADMIN")
                .and().withUser("user").password(password).roles("USER");
    }
}

    (6)测试

    启动项目,登录user用户访问localhost:8080/user/addUser  则控制台输出提示:

    addUser

    访问http://localhost:8080/user/deleteUser  则会提示没有权限:

    

  实例:实现RBAC权限模型

    本实例介绍在Spring Security配置类上配置自定义授权策略,可以通过加入access属性和 URL判断来实现RBAC权限模型的核心功能。

    RBAC模型简化了用户和权限的关系。通过角色对用户进行分组,分组后可以很方便地逬行权 限分配与管理。RBAC模型易扩展和维护。下面介绍具体步骤

    (1)创建RBAC验证服务接口。

    用于权限检查,见以下代码

public interface RabeService {
    boolean check(HttpServletRequest request, Authentication authentication);
}

    (2)编写RBAC服务实现,判断URL是否在权限表中

    要实现RBAC服务,步骤如下:

  • 通过注入用户和该用户所拥有的权限(权限任登录成功时已经缓存起来,当需要访问该用户的权限时,直接从缓存取岀)验证该请求是否有权限,有就返回true,没有则返回false,不允许访问该URL。
  • 传入request,可以使用request获取该次请求的类型。
  • 根据Restful风格使用它来控制的权限。如请求是POST,则证明该请求是向服务器发送一 个新建资源请求,可以使用getMethod()来获取该请求的方式。
  • 配合角色所允许的权限路径进行判断和授权操作。
  • 如果获取到的Principal对象不为空,则代表授权已经通过。

  本实例不针对HTTP请求进行判断,只根据URL逬行鉴权,具体代码如下

@Component("rabcService")
public class RabeServiceImpl implements RabeService {
    private AntPathMatcher antPathMatcher = new AntPathMatcher();
    @Autowired
    private SysPermissionRepository permissionRepository;
    @Autowired
    private SysUserRepository sysUserRepository;
    @Override
    public boolean check(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        boolean hasPermission = false;
        if (principal!=null && principal instanceof UserDetails){
            String userName = ((UserDetails) principal).getUsername();
            Set<String> urls = new HashSet<String>();
            SysUser sysUser = sysUserRepository.findByName(userName);
            try {
                for (SysRole role : sysUser.getRoles()) {
                    for (SysPermission permission: role.getPermissions()) {
                        urls.add(permission.getUrl());
                    }
                }
            }catch (Exception e) {
                e.printStackTrace();
            }
            for (String url : urls){
                if (AntPathMatcher.match(url, request.getRequestURI())){
                    hasPermission = true;
                    break;
                }
            }
        }
        return hasPermission;
    }
}

    (3)配置 HttpSecurity

    在继承 WebSecurityConfigurerAdapter 的类中重写 void configure(HttpSecurity http)方法,添加如下代码:

    .antMatchers("/admin/**").access("@rabcService.check(request,authentication)") 

    这里注意,@rbacService接口的名字是服务实现上定义的名字,即注解@Component(“rbacService”)定义的参数。具体代码如下

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/admin").permitAll()
            .antMatchers("/admin/rabc").access("@rabcService.check(request,authentication)")
            .and()
            .formLogin();
}

    (4)创建实体,添加测试数据

    这里要创建3个实体,分别是用户【sys_user】(id、cnname、enabled、name、password)、权限【sys_permission】(id、available、name、parent_id、parent_ids、permission、resource_type、url)和角色实体【sys_role】(id、available、cnname、description、role),另外还需创建两张表【sys_role_permission】(role_id、permission_id)、【sys_user_role】(role_id、uid)

    (5)启动项目后进行测试

  • 访问http://localhost:8080/admin/rbac,会提示无权访问,跳转到登录页面,http:”localhost: 8080/admin/login”
  • 在登录页面输入用户名、密码(admin/lzh)登录,会提示登录成功。
  • 访问”http://localhost:8080/admin/rbac”, 提示访问成功