Spring Security OAuth2 JWT之快速入门篇(个人笔记)

1.基本概念

1.1.什么是认证

        进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。

系统为什么要认证?

        认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。

认证 :用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信 息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手 机短信登录,指纹认证等方式。

1.2 什么是会话

        用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。

基于session的认证方式如下图:
在这里插入图片描述

        它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的 sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数 据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。

基于token方式如下图:
在这里插入图片描述
        它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。

总结:基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持 cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。

1.2 什么是授权

        还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。

为什么要授权?
        认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。

授权: 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

1.3 授权的数据模型

        如何进行授权即如何对用户访问资源进行控制,首先需要学习授权相关的数据模型。

授权可简单理解为Who对What(which)进行How操作,包括如下:

Who,即主体(Subject),主体一般是指用户,也可以是程序,需要访问系统中的资源。 What,即资源 (Resource),如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、页面、按 钮、代码方法都属于系统功能资源,对于web系统每个功能资源通常对应一个URL;系统商品信息、系统订单信息 都属于实体资源(数据资源),实体资源由资源类型和资源实例组成,比如商品信息为资源类型,商品编号 为001 的商品为资源实例。 How,权限/许可(Permission),规定了用户对资源的操作许可,权限离开资源没有意义, 如用户查询权限、用户添加权限、某个代码方法的调用权限、编号为001的用户的修改权限等,通过权限可知用户 对哪些资源都有哪些操作许可。

主体、资源、权限关系如下图:
在这里插入图片描述

主体、资源、权限相关的数据模型如下:

主体(用户id、账号、密码、…)
资源(资源id、资源名称、访问地址、…)
权限(权限id、权限标识、权限名称、资源id、…)
角色(角色id、角色名称、…)
角色和权限关系(角色id、权限id、…)
主体(用户)和角色关系(用户id、角色id、…)

主体(用户)、资源、权限关系如下图:
在这里插入图片描述
通常企业开发中将资源和权限表合并为一张权限表,如下:

资源(资源id、资源名称、访问地址、…)
权限(权限id、权限标识、权限名称、资源id、…)

合并为: 权限(权限id、权限标识、权限名称、资源名称、资源访问地址、…)

修改后数据模型之间的关系如下图:
在这里插入图片描述

1.4 RBAC

如何实现授权?业界通常基于RBAC实现授权。

1.4.1 基于角色的访问控制

        RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:
在这里插入图片描述
根据上图中的判断逻辑,授权代码可表示如下:

if(主体.hasRole(“总经理角色id”)){
    查询工资
}

如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是 总经理或部门经理”,修改代码如下:

if(主体.hasRole(“总经理角色id”) || 主体.hasRole(“部门经理角色id”)){
    查询工资
}

根据上边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。

1.4.2 基于资源(权限)的访问控制

        RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须 有查询工资权限才可以查询员工工资信息等,访问控制流程如下:
在这里插入图片描述
根据上图中的判断,授权代码可以表示为:

if(主体.hasPermission(“查询工资权限标识”)){
    查询工资
}

优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。

2.基于Session的认证方式

2.1 认证流程

        基于Session认证方式的流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话),而发 给客户端的 sesssion_id 存放到 cookie 中,这样用客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验。当用户退出系统或session过期销毁时,客户端的session_id也就无效了。 下图是session认证方式的流程图:
在这里插入图片描述
基于Session的认证机制由Servlet规范定制,Servlet容器已实现,用户通过HttpSession的操作方法即可实现,如 下是HttpSession相关的操作API。

方法含义
HttpSession getSession(Boolean create)获取当前HttpSession对象
void setAttribute(String name,Object value)向session中存放对象
object getAttribute(String name)从session中获取对象
void removeAttribute(String name);移除session中对象
void invalidate()使HttpSession失效
略…

2.2.创建工程

本案例工程使用maven进行构建,使用SpringMVC、Servlet3.0实现。

2.2.1 创建maven工程

创建maven工程 security-springmvc,工程结构如下:
在这里插入图片描述
pom.xml文件引入如下依赖如下,注意: 1、由于是web工程,packaging设置为war 2、使用tomcat7-maven-plugin插件来运行工程。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.king.security</groupId>
    <artifactId>security-springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>security-springmvc</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.maven</groupId>
                    <artifactId>tomcat7-maven-plugin</artifactId>
                    <version>2.2</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>

                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <configuration>
                        <encoding>utf-8</encoding>
                        <useDefaultDelimiters>true</useDefaultDelimiters>
                        <resources>
                            <resource>
                                <directory>src/main/resources</directory>
                                <filtering>true</filtering>
                                <includes>
                                    <include>**/*</include>
                                </includes>
                            </resource>
                            <resource>
                                <directory>src/main/java</directory>
                                <includes>
                                    <include>**/*.xml</include>
                                </includes>
                            </resource>
                        </resources>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

2.2.2 Spring 容器配置

config包下定义ApplicationConfig.java,它对应web.xmlContextLoaderListener的配置

package com.king.security.springmvc.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;

/**
 * 加载Spring容器,相当于applicationContext.xml
 **/
@Configuration
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
            ,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) // 排除Controller层不进行扫描,在WebConfig类进行扫描
public class ApplicationConfig {
    //在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、aop、业务bean等。
}

2.2.3 servletContext配置

本案例采用Servlet3.0web.xml方式,的config包下定义WebConfig.java,它对应于DispatcherServlet配置。

package com.king.security.springmvc.config;

import com.king.security.springmvc.interceptor.SimpleAuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * ServletContext,就相当于springmvc.xml文件
 **/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})  // 扫描Controller层
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    /**
     * 视频解析器
     * @return
     */
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    /**
     * 配置页面映射列表
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // addViewController:URL访问路径
        // setViewName:映射的页面
        registry.addViewController("/").setViewName("login");
    }

    /**
     * 配置拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // addInterceptor:需要一个实现 HandlerInterceptor 接口的拦截器实例。
        // addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截。
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");

        // excludePathPatterns:用于设置不需要拦截的过滤规则。
        registry.addInterceptor(simpleAuthenticationInterceptor).excludePathPatterns("/","/login","/js/**","/css/**","/images/**");
    }

    /**
     * 配置跨域
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
                .allowCredentials(true) // 允许带cookie访问
                .allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
                .allowedHeaders("token")
                .maxAge(3600);
    }
}

2.2.4 加载 Spring容器

init包下定义Spring容器初始化类SpringApplicationInitializer,此类实现WebApplicationInitializer接口, Spring容器启动时加载WebApplicationInitializer接口的所有实现类。

package com.king.security.springmvc.init;

import com.king.security.springmvc.config.ApplicationConfig;
import com.king.security.springmvc.config.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * 加载Spring容器以及初始化,就相当于web.xml,使用了servlet3.0开发则不需要再定义web.xml
 **/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * spring容器,相当于加载 applicationContext.xml
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ApplicationConfig.class};
    }

    /**
     * servletContext,相当于加载springmvc.xml
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    /**
     * url-mapping
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * web.xml参考:
     * <web‐app>
     *        <listener>
     *            <listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
     *        </listener>
     *        <context‐param>
     *            <param‐name>contextConfigLocation</param‐name>
     *            <param‐value>/WEB‐INF/application‐context.xml</param‐value>
     *        </context‐param>
     *        <servlet>
     *            <servlet‐name>springmvc</servlet‐name>
     *            <servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
     *            <init‐param>
     *                <param‐name>contextConfigLocation</param‐name>
     *                <param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
     *            </init‐param>
     *            <load‐on‐startup>1</load‐on‐startup>
     *        </servlet>
     *        <servlet‐mapping>
     *            <servlet‐name>springmvc</servlet‐name>
     *            <url‐pattern>/</url‐pattern>
     *        </servlet‐mapping>
     *    </web‐app>
     */
}

SpringApplicationInitializer相当于web.xml,使用了servlet3.0开发则不需要再定义web.xmlApplicationConfig.class对应以下配置的application-context.xmlWebConfig.class对应以下配置的spring- mvc.xmlweb.xml的内容参考:

<web‐app>
       <listener>
           <listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
       </listener>
       <context‐param>
           <param‐name>contextConfigLocation</param‐name>
           <param‐value>/WEB‐INF/application‐context.xml</param‐value>
       </context‐param>
       <servlet>
           <servlet‐name>springmvc</servlet‐name>
           <servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
           <init‐param>
               <param‐name>contextConfigLocation</param‐name>
               <param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
           </init‐param>
           <load‐on‐startup>1</load‐on‐startup>
       </servlet>
       <servlet‐mapping>
           <servlet‐name>springmvc</servlet‐name>
           <url‐pattern>/</url‐pattern>
       </servlet‐mapping>
   </web‐app>

2.3.实现认证功能

2.3.1 认证页面

webapp/WEB-INF/views下定义认证页面login.jsp,本案例只是测试认证流程,页面没有添加css样式,页面实现可填入用户名,密码,触发登录将提交表单信息至/login,内容如下:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
    <title>用户登录</title>
</head>
<body>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>&nbsp;&nbsp;&nbsp;码:
    <input type="password" name="password"><br>
    <input type="submit" value="登录">
</form>
</body>
</html>

WebConfig中新增如下配置,将/直接导向login.jsp页面:

/**
  * 配置页面映射列表
  * @param registry
  */
 @Override
 public void addViewControllers(ViewControllerRegistry registry) {
     // addViewController:URL访问路径
     // setViewName:映射的页面
     registry.addViewController("/").setViewName("login");
 }

启动项目,访问/路径地址,进行测试:

在这里插入图片描述

2.3.2 认证接口

用户进入认证页面,输入账号和密码,点击登录,请求/login进行身份认证。
(1)定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出错误异常:

package com.king.security.springmvc.service;

import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;

/**
 * 认证服务
 */
public interface AuthenticationService {
    /**
     * 用户认证
     * @param authenticationRequest 用户认证请求,账号和密码
     * @return 认证成功的用户信息
     */
    UserDto authentication(AuthenticationRequest authenticationRequest);
}

认证请求结构:

package com.king.security.springmvc.model;

import lombok.Data;

/**
 *
 **/
@Data
public class AuthenticationRequest {
    //认证请求参数,账号、密码。。
    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

}

认证成功后返回的用户详细信息,也就是当前登录用户的信息:

package com.king.security.springmvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

/**
 * 用户信息
 **/
@Data
@AllArgsConstructor
public class UserDto {

    //用户身份信息
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

(2)认证实现类,根据用户名查找用户信息,并校验密码,这里模拟了两个用户:

package com.king.security.springmvc.service.impl;

import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 认证服务实现类
 **/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
	
	/**
     * 用户认证,校验用户身份信息是否合法
     *
     * @param authenticationRequest 用户认证请求,账号和密码
     * @return 认证成功的用户信息
     */
    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
        //校验参数是否为空
        if(authenticationRequest == null
            || StringUtils.isEmpty(authenticationRequest.getUsername())
            || StringUtils.isEmpty(authenticationRequest.getPassword())){
            throw new RuntimeException("账号和密码为空");
        }

        //根据账号去查询数据库,这里测试程序采用模拟方法
        UserDto user = getUserDto(authenticationRequest.getUsername());

        //判断用户是否为空
        if(user == null){
            throw new RuntimeException("查询不到该用户");
        }
        //校验密码
        if(!authenticationRequest.getPassword().equals(user.getPassword())){
            throw new RuntimeException("账号或密码错误");
        }
        //认证通过,返回用户身份信息
        return user;
    }

    //根据账号查询用户信息
    private UserDto getUserDto(String userName){
        return userMap.get(userName);
    }

    /**
     * 用于存储用户信息
     */
    private Map<String,UserDto> userMap = new HashMap<>();

    /**
     * 构造代码块,初始化用户信息
     */
    {
        userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443"));
        userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553"));
    }
}

(3)登录Controller,对/login请求处理,它调用AuthenticationService完成认证并返回登录结果提示信息:

package com.king.security.springmvc.controller;

import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
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;

import javax.servlet.http.HttpSession;

/**
 * 登录控制层
 **/
@RestController
public class LoginController {

    @Autowired
    AuthenticationService authenticationService;

    @RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
    public String login(AuthenticationRequest authenticationRequest, HttpSession session){
        UserDto userDto = authenticationService.authentication(authenticationRequest);
        //存入session
        session.setAttribute(UserDto.SESSION_USER_KEY,userDto);

        System.out.println("session:"+session.getAttribute(UserDto.SESSION_USER_KEY));
        return userDto.getUsername() +"登录成功";
    }

}

(5)测试 启动项目,访问/路径地址,进行测试
在这里插入图片描述
填入错误的用户信息,页面返回错误信息:
在这里插入图片描述
填入正确的用户信息,页面提示登录成功:
在这里插入图片描述
以上的测试全部符合预期,到目前为止最基础的认证功能已经完成,它仅仅实现了对用户身份凭证的校验,若某用户认证成功,只能说明他是该系统的一个合法用户,仅此而已。

2.4.实现会话功能

会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。

认证的目的是对系统资源的保护,每次对资源的访问,系统必须得知道是谁在访问资源,才能对该请求进行合法性 拦截。因此,在认证成功后,一般会把认证成功的用户信息放入Session中,在后续的请求中,系统能够从Session 中获取到当前用户,用这样的方式来实现会话机制。

(1)增加会话控制
首先在UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key。

public static final String SESSION_USER_KEY = "_user";

然后修改LoginController,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为失效。

package com.king.security.springmvc.controller;

import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
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;

import javax.servlet.http.HttpSession;

/**
 * 登录控制层
 **/
@RestController
public class LoginController {

    @Autowired
    AuthenticationService authenticationService;

    @RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
    public String login(AuthenticationRequest authenticationRequest, HttpSession session){
        UserDto userDto = authenticationService.authentication(authenticationRequest);
        //存入session
        session.setAttribute(UserDto.SESSION_USER_KEY,userDto);

        System.out.println("session:"+session.getAttribute(UserDto.SESSION_USER_KEY));
        return userDto.getUsername() +"登录成功";
    }

    @GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
    public String logout(HttpSession session){
        session.invalidate();
        return "退出成功";
    }
}    

(2)增加测试资源
修改LoginController,增加测试资源/r/r1,它从当前会话session中获取当前登录用户,并返回提示信息给前台。

package com.king.security.springmvc.controller;

import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
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;

import javax.servlet.http.HttpSession;

/**
 * 登录控制层
 **/
@RestController
public class LoginController {

    @Autowired
    AuthenticationService authenticationService;

    @RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
    public String login(AuthenticationRequest authenticationRequest, HttpSession session){
        UserDto userDto = authenticationService.authentication(authenticationRequest);
        //存入session
        session.setAttribute(UserDto.SESSION_USER_KEY,userDto);

        System.out.println("session:"+session.getAttribute(UserDto.SESSION_USER_KEY));
        return userDto.getUsername() +"登录成功";
    }

    @GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
    public String logout(HttpSession session){
        session.invalidate();
        return "退出成功";
    }

    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(HttpSession session){
        String fullname = null;
        Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
        if(object == null){
            fullname = "匿名";
        }else{
            UserDto userDto = (UserDto) object;
            fullname = userDto.getFullname();
        }
        return fullname+"访问资源r1";
    }
}    

(3)测试
未登录情况下直接访问测试资源/r/r1:
在这里插入图片描述
成功登录的情况下访问测试资源/r/r1:
在这里插入图片描述
测试结果说明,在用户登录成功时,该用户信息已被成功放入session,并且后续请求可以正常从session中获取当 前登录用户信息,符合预期结果。

2.5.实现授权功能

现在我们已经完成了用户身份凭证的校验以及登录的状态保持,并且我们也知道了如何获取当前登录用户(从Session中获取)的信息,接下来,用户访问系统需要经过授权,即需要完成如下功能:

  • 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
  • 登录用户访问拦截:根据用户的权限决定是否能访问某些资源。

(1)增加权限数据为了实现这样的功能,我们需要在UserDto里增加权限属性,用于表示该登录用户所拥有的权限,同时修改 UserDto的构造方法。

package com.king.security.springmvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

/**
 * 用户信息
 **/
@Data
@AllArgsConstructor
public class UserDto {
    public static final String SESSION_USER_KEY = "_user";
    //用户身份信息
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
    /**
     * 用户权限
     */
    private Set<String> authorities;
}

并在AuthenticationServiceImpl中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限

package com.king.security.springmvc.service.impl;

import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 认证服务实现类
 **/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
    /**
     * 用户认证,校验用户身份信息是否合法
     *
     * @param authenticationRequest 用户认证请求,账号和密码
     * @return 认证成功的用户信息
     */
    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
        //校验参数是否为空
        if(authenticationRequest == null
            || StringUtils.isEmpty(authenticationRequest.getUsername())
            || StringUtils.isEmpty(authenticationRequest.getPassword())){
            throw new RuntimeException("账号和密码为空");
        }

        //根据账号去查询数据库,这里测试程序采用模拟方法
        UserDto user = getUserDto(authenticationRequest.getUsername());

        //判断用户是否为空
        if(user == null){
            throw new RuntimeException("查询不到该用户");
        }
        //校验密码
        if(!authenticationRequest.getPassword().equals(user.getPassword())){
            throw new RuntimeException("账号或密码错误");
        }
        //认证通过,返回用户身份信息
        return user;
    }

    //根据账号查询用户信息
    private UserDto getUserDto(String userName){
        return userMap.get(userName);
    }

    /**
     * 用于存储用户信息
     */
    private Map<String,UserDto> userMap = new HashMap<>();

    /**
     * 构造代码块,初始化用户信息
     */
    {
        Set<String> authorities1 = new HashSet<>();
        authorities1.add("p1");//这个p1我们人为让它和/r/r1对应
        Set<String> authorities2 = new HashSet<>();
        authorities2.add("p2");//这个p2我们人为让它和/r/r2对应
        userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443",authorities1));
        userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
    }
}

(3)实现授权拦截器
interceptor包下定义SimpleAuthenticationInterceptor拦截器,实现授权拦截:
1、校验用户是否登录
2、校验用户是否拥有操作权限

package com.king.security.springmvc.interceptor;

import com.king.security.springmvc.model.UserDto;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 自定义拦截器
 **/
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
    /**
     * 前置拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //在这个方法中校验用户请求的url是否在用户的权限范围内
        //取出用户身份信息
        Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
        if(object == null){
            //没有认证,提示登录
            writeContent(response,"请登录");
        }
        UserDto userDto = (UserDto) object;
        //请求的url
        String requestURI = request.getRequestURI();
        if( (userDto.getAuthorities().contains("p1") && requestURI.contains("/r/r1")) || (userDto.getAuthorities().contains("p1") && requestURI.contains("/logout"))){
            return true;
        }
        if( userDto.getAuthorities().contains("p2") && requestURI.contains("/r/r2")){
            return true;
        }
        writeContent(response,"没有权限,拒绝访问");

        return false;
    }

    //响应信息给客户端
    private void writeContent(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.print(msg);
        writer.close();
    }

}

WebConfig中配置拦截器,匹配/r/**的资源为受保护的系统资源,访问该资源的请求进入 SimpleAuthenticationInterceptor拦截器。

package com.king.security.springmvc.config;

import com.king.security.springmvc.interceptor.SimpleAuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * ServletContext,就相当于springmvc.xml文件
 **/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})  // 扫描Controller层
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    /**
     * 视频解析器
     * @return
     */
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    /**
     * 配置页面映射列表
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // addViewController:URL访问路径
        // setViewName:映射的页面名称
        registry.addViewController("/").setViewName("login");
    }

    /**
     * 配置拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // addInterceptor:需要一个实现 HandlerInterceptor 接口的拦截器实例。
        // addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截。
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");

        // excludePathPatterns:用于设置不需要拦截的过滤规则。
        registry.addInterceptor(simpleAuthenticationInterceptor).excludePathPatterns("/","/login","/js/**","/css/**","/images/**");
    }

    /**
     * 配置跨域
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
                .allowCredentials(true) // 允许带cookie访问
                .allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
                .allowedHeaders("token")
                .maxAge(3600);
    }
}

(4)测试
未登录情况下,/r/r1与/r/r2均提示 “请先登录”。
在这里插入图片描述
张三登录情况下,由于张三有p1权限,因此可以访问/r/r1,张三没有p2权限,访问/r/r2时提示 “权限不足 “。

在这里插入图片描述
在这里插入图片描述
李四登录情况下,由于李四有p2权限,因此可以访问/r/r2,李四没有p1权限,访问/r/r1时提示 “权限不足 “。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
测试结果全部符合预期结果。

2.6.小结

        基于Session的认证方式是一种常见的认证方式,至今还有非常多的系统在使用。我们在此小节使用Spring mvc技 术对它进行简单实现,旨在让大家更清晰实在的了解用户认证、授权以及会话的功能意义及实现套路,也就是它们 分别干了哪些事儿?大概需要怎么做?

        而在正式生产项目中,我们往往会考虑使用第三方安全框架(如 spring security,shiro等安全框架)来实现认证 授权功能,因为这样做能一定程度提高生产力,提高软件标准化程度,另外往往这些框架的可扩展性考虑的非常全 面。但是缺点也非常明显,这些通用化组件为了提高支持范围会增加很多可能我们不需要的功能,结构上也会比较 抽象,如果我们不够了解它,一旦出现问题,将会很难定位。

3.Spring Security快速上手

3.1 Spring Security介绍

        Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它 是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入spring security更是十分简单,使用Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。

3.2 创建工程

3.2.1 创建maven工程

创建maven工程 security-springsecurity,工程结构如下:
在这里插入图片描述

2)引入以下依赖:
security-springmvc的项目基础上增加spring-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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.king.security</groupId>
    <artifactId>security-springsecurity</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- 引入SpringSecurity依赖 -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>5.1.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>5.1.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>security-springsecurity</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.maven</groupId>
                    <artifactId>tomcat7-maven-plugin</artifactId>
                    <version>2.2</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>

                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <configuration>
                        <encoding>utf-8</encoding>
                        <useDefaultDelimiters>true</useDefaultDelimiters>
                        <resources>
                            <resource>
                                <directory>src/main/resources</directory>
                                <filtering>true</filtering>
                                <includes>
                                    <include>**/*</include>
                                </includes>
                            </resource>
                            <resource>
                                <directory>src/main/java</directory>
                                <includes>
                                    <include>**/*.xml</include>
                                </includes>
                            </resource>
                        </resources>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

3.2.2 Spring容器配置

同security-springmvc项目一样.

package com.king.security.springmvc.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;

/**
 * 加载Spring容器,相当于applicationContext.xml
 **/
@Configuration
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
        ,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) // 排除Controller层不进行扫描,在WebConfig类进行扫描
public class ApplicationConfig {
    //在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、aop、业务bean等。
}

3.2.3 Servlet Context配置

同security-springmvc项目一样.

package com.king.security.springmvc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * ServletContext,就相当于springmvc.xml文件
 **/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})  // 扫描Controller层
public class WebConfig implements WebMvcConfigurer {


    /**
     * 视频解析器
     * @return
     */
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    /**
     * 配置页面映射列表
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // addViewController:URL访问路径
        // setViewName:映射的页面名称
        registry.addViewController("/").setViewName("login");
    }

    /**
     * 配置拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 这里使用SpringSecurity做认证、授权、拦截功能,就不用自定义拦截器了
    }

    /**
     * 配置跨域
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
                .allowCredentials(true) // 允许带cookie访问
                .allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
                .allowedHeaders("token")
                .maxAge(3600);
    }
}

3.2.4 加载 Spring容器

init包下定义Spring容器初始化类SpringApplicationInitializer,此类实现WebApplicationInitializer接口,Spring容器启动时加载WebApplicationInitializer接口的所有实现类。

package com.king.security.springmvc.init;

import com.king.security.springmvc.config.ApplicationConfig;
import com.king.security.springmvc.config.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * 加载Spring容器以及初始化,就相当于web.xml,使用了servlet3.0开发则不需要再定义web.xml
 **/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * spring容器,相当于加载 applicationContext.xml
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ApplicationConfig.class};
    }

    /**
     * servletContext,相当于加载springmvc.xml
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    /**
     * url-mapping
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * web.xml参考:
     * <web‐app>
     *        <listener>
     *            <listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
     *        </listener>
     *        <context‐param>
     *            <param‐name>contextConfigLocation</param‐name>
     *            <param‐value>/WEB‐INF/application‐context.xml</param‐value>
     *        </context‐param>
     *        <servlet>
     *            <servlet‐name>springmvc</servlet‐name>
     *            <servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
     *            <init‐param>
     *                <param‐name>contextConfigLocation</param‐name>
     *                <param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
     *            </init‐param>
     *            <load‐on‐startup>1</load‐on‐startup>
     *        </servlet>
     *        <servlet‐mapping>
     *            <servlet‐name>springmvc</servlet‐name>
     *            <url‐pattern>/</url‐pattern>
     *        </servlet‐mapping>
     *    </web‐app>
     */
}

3.3 认证

3.3.1 认证页面

spring security默认提供认证页面,不需要额外开发。
在这里插入图片描述

3.3.2.安全配置

spring security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。

1)在config包下定义WebSecurityConfig,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。

package com.king.security.springmvc.config;

import org.springframework.context.annotation.Bean;
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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * spring security安全配置
 **/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 定义用户信息认证服务(认证)
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        // 添加认证放行的用户信息并设置权限
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1","p3").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    /**
     * 密码编码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        // 无需编码器进行匹配密码
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // //开启登录配置
                .antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
                .antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }
}

userDetailsService()方法中,我们返回了一个UserDetailsService给spring容器,Spring Security会使用它来获取用户信息。我们暂时使用InMemoryUserDetailsManager实现类,并在其中分别创建了zhangsanlisi两个用户,并设置密码和权限。

而在configure()安全拦截机制方法中,我们通过HttpSecurity设置了安全拦截规则,其中包含了以下内容:

(1)url匹配/r/**的资源,经过认证后才能访问。
(2)其他url完全开放。
(3)支持form表单认证,认证成功后转向/login-success。

关于HttpSecurity的配置清单请参考附录 HttpSecurity。

2)让 Spring 容器加载 WebSecurityConfig 配置
修改SpringApplicationInitializer类的getRootConfigClasses()方法,添加WebSecurityConfig.class

package com.king.security.springmvc.init;

import com.king.security.springmvc.config.ApplicationConfig;
import com.king.security.springmvc.config.WebConfig;
import com.king.security.springmvc.config.WebSecurityConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * 加载Spring容器以及初始化,就相当于web.xml,使用了servlet3.0开发则不需要再定义web.xml
 **/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * spring容器,相当于加载 applicationContext.xml
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        // 加载ApplicationConfig类、WebSecurityConfig类
        return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
    }

    /**
     * servletContext,相当于加载springmvc.xml
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    /**
     * url-mapping
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * web.xml参考:
     * <web‐app>
     *        <listener>
     *            <listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
     *        </listener>
     *        <context‐param>
     *            <param‐name>contextConfigLocation</param‐name>
     *            <param‐value>/WEB‐INF/application‐context.xml</param‐value>
     *        </context‐param>
     *        <servlet>
     *            <servlet‐name>springmvc</servlet‐name>
     *            <servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
     *            <init‐param>
     *                <param‐name>contextConfigLocation</param‐name>
     *                <param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
     *            </init‐param>
     *            <load‐on‐startup>1</load‐on‐startup>
     *        </servlet>
     *        <servlet‐mapping>
     *            <servlet‐name>springmvc</servlet‐name>
     *            <url‐pattern>/</url‐pattern>
     *        </servlet‐mapping>
     *    </web‐app>
     */
}

3.3.2.Spring Security初始化

Spring Security初始化,这里有两种情况

  • 若当前环境没有使用SpringSpring MVC,则需要将WebSecurityConfig(Spring Security配置类) 传入超类,以确保获取配置,并创建spring context
  • 相反,若当前环境已经使用spring,我们应该在现有的springContext中注册Spring Security(上一步已经做将 WebSecurityConfig 加载至 rootcontext),此方法可以什么都不做。 在init包下定义SpringSecurityApplicationInitializer
package com.king.security.springmvc.init;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

/**
 * 初始化Spring Security配置
 **/
public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    public SpringSecurityApplicationInitializer() {
        /**
         * 如果当前环境下没有使用 Spring 或 SpringMVC,则需要将 WebSecurityConfig(Spring Security配置类) 传入超类,以确保获取配置,并创建spring context。
         * 相反,若当前环境已经使用spring,应该在现有的springContext中注册Spring Security(上一步已经做将WebSecurityConfig加载至RootConfigClasses),此方法可以什么都不做。
         */
        //super(WebSecurityConfig.class);
    }
}

3.2.3.默认根路径请求

WebConfig.java中添加默认请求根路径跳转到/login,此url为spring security提供:

package com.king.security.springmvc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * ServletContext,就相当于springmvc.xml文件
 **/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})  // 扫描Controller层
public class WebConfig implements WebMvcConfigurer {


    /**
     * 视频解析器
     * @return
     */
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    /**
     * 配置页面映射列表
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 默认Url根路径跳转到/login,此url为spring security提供的登录页
        // addViewController:URL访问路径
        // setViewName:映射的页面名称
        // 重定向到/login页面去
        registry.addViewController("/").setViewName("redirect:/login");
    }

    /**
     * 配置拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 这里使用SpringSecurity做认证、授权、拦截功能,就不用自定义拦截器了
    }

    /**
     * 配置跨域
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
                .allowCredentials(true) // 允许带cookie访问
                .allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
                .allowedHeaders("token")
                .maxAge(3600);
    }
}

spring security默认提供的登录页面。

3.2.4.认证成功页面

在安全配置中,认证成功将跳转到/login-success,代码如下:

	/**
     * 安全拦截机制(最重要)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // //开启登录配置
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }

spring security支持form表单认证,认证成功后转向/login-success。 在LoginController中定义/login-success:

package com.king.security.springmvc.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 登录控制层
 **/
@RestController
public class LoginController {
    /**
     * 认证成功将跳转到/login-success
     * @return
     */
    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        return " 登录成功";
    }
}    

3.2.5 测试

(1)启动项目,访问 http://localhost:8080/security-springsecurity/login 路径地址:
在这里插入图片描述

页面会根据WebConfigaddViewControllers配置规则,跳转至/login/loginSpring Security提供的登录页面。

(2)登录
1、输入错误的用户名、密码
在这里插入图片描述

2、输入正确的用户名、密码,登录成功
在这里插入图片描述

(3)退出
1、请求 http://localhost:8080/security-springsecurity/logout 退出
在这里插入图片描述

2、退出后再访问资源自动跳转到登录页面
在这里插入图片描述

3.4 授权

        实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源,Spring Security默认提供授权实现方法。

LoginController 类添加 /r/r1 方法或 /r/r2 方法:

   /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(){
        return " 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    public String r2(){
        return " 访问资源2";
    }

在安全配置类 WebSecurityConfig.java 中配置授权规则:

.antMatchers("/r/r1").hasAuthority(“p1”)
.antMatchers("/r/r2").hasAuthority(“p2”)

.antMatchers("/r/r1").hasAuthority("p1")表示:访问/r/r1资源的 url需要拥有p1权限。
.antMatchers("/r/r2").hasAuthority("p2")表示:访问/r/r2资源的 url需要拥有p2权限。

完整的WebSecurityConfig配置类的 configure() 方法如下:

   /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // //开启登录配置
                .antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
                .antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }

测试:
1、登录成功
2、访问/r/r1和/r/r2,有权限时则正常访问,否则返回403(拒绝访问)
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.5 小结

        通过快速上手,咱们使用Spring Security实现了认证和授权,Spring Security提供了基于账号和密码的认证方式,通过安全配置即可实现请求拦截,授权功能,Spring Security能完成的不仅仅是这些。

4.Spring Security 应用详解

4.1 集成SpringBoot

4.1.1 Spring Boot 介绍

        Spring Boot是一套Spring的快速开发框架,基于Spring 4.0设计,使用Spring Boot开发可以避免一些繁琐的工程搭建和配置,同时它集成了大量的常用框架,快速导入依赖包,避免依赖包的冲突。基本上常用的开发框架都支持 Spring Boot 开发,例如:MyBatisDubbo等,Spring 家族更是如此,例如:Spring cloudSpring mvcSpring security等,使用Spring Boot开发可以大大得高生产率,所以Spring Boot的使用率非常高。

        本章节讲解如何通过Spring Boot开发Spring Security应用,Spring Boot提供spring-boot-starter-security用于开发Spring Security应用。

4.1.2 创建maven工程

1)创建maven工程 security-springboot,工程结构如下:
在这里插入图片描述

2)引入以下依赖:

<?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.6.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<groupId>com.king.security</groupId>
	<artifactId>security-springboot</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<name>security-springboot</name>
	<description>security-springboot</description>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>

	<dependencies>
		<!-- 以下是>spring boot依赖-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!-- 以下是>spring security依赖-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

		<!-- 以下是jsp依赖-->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<scope>provided</scope>
		</dependency>
		<!-- jsp页面使用jstl标签 -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
		</dependency>
		<!-- 用于编译jsp -->
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.0</version>
		</dependency>

		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.13.1</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.47</version>
		</dependency>
	</dependencies>

	<build>
		<finalName>security-springboot</finalName>
		<pluginManagement>
			<plugins>
				<plugin>
					<groupId>org.apache.tomcat.maven</groupId>
					<artifactId>tomcat7-maven-plugin</artifactId>
					<version>2.2</version>
				</plugin>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-compiler-plugin</artifactId>
					<configuration>
						<source>1.8</source>
						<target>1.8</target>
					</configuration>
				</plugin>

				<plugin>
					<artifactId>maven-resources-plugin</artifactId>
					<configuration>
						<encoding>utf-8</encoding>
						<useDefaultDelimiters>true</useDefaultDelimiters>
						<resources>
							<resource>
								<directory>src/main/resources</directory>
								<filtering>true</filtering>
								<includes>
									<include>**/*</include>
								</includes>
							</resource>
							<resource>
								<directory>src/main/java</directory>
								<includes>
									<include>**/*.xml</include>
								</includes>
							</resource>
						</resources>
					</configuration>
				</plugin>
			</plugins>
		</pluginManagement>
	</build>

</project>

4.1.3 spring 容器配置

SpringBoot 工程启动会自动扫描启动类所在包下的所有 Bean,加载到 spring 容器。

1)Spring Boot配置文件
resources下添加application.properties,内容如下:

# 端口号
server.port=8080
# 默认的URL上下文路径
server.servlet.context-path=/security-springboot
# 应用程序名称
spring.application.name=security-springboot

# 页面映射
spring.mvc.view.prefix=/WEB-INF/view/
spring.mvc.view.suffix=.jsp

# 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=rootroot

2)Spring Boot 启动类

package com.king.security.securityspringboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecuritySpringbootApplication {

	public static void main(String[] args) {
		SpringApplication.run(SecuritySpringbootApplication.class, args);
	}

}

4.1.4 Servlet Context配置

由于Spring boot starter自动装配机制,这里无需使用@EnableWebMvc@ComponentScanWebConfig配置文件如下:

package com.king.security.securityspringboot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

/**
 * ServletContext,就相当于springmvc.xml文件
 **/
@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * 配置页面映射列表
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 默认Url根路径跳转到/login,此url为spring security提供的登录页
        // addViewController:URL访问路径
        // setViewName:映射的页面名称
        // 重定向到/login页面去
        registry.addViewController("/").setViewName("redirect:/login");
    }

    /**
     * 配置拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 这里使用SpringSecurity做认证、授权、拦截功能,就不用自定义拦截器了
    }

    /**
     * 配置跨域
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
                .allowCredentials(true) // 允许带cookie访问
                .allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
                .allowedHeaders("token")
                .maxAge(3600);
    }
}

视图解析器配置在 application.properties 文件中:

# 端口号
server.port=8080
# 默认的URL上下文路径
server.servlet.context-path=/security-springboot
# 应用程序名称
spring.application.name=security-springboot

# 页面映射
spring.mvc.view.prefix=/WEB-INF/view/
spring.mvc.view.suffix=.jsp

# 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=rootroot

4.1.5 安全配置

由于 Spring boot starter 自动装配机制,这里无需使用@EnableWebSecurityWebSecurityConfig配置文件内容如下:

package com.king.security.securityspringboot.config;

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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * spring security安全配置
 **/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 定义用户信息认证服务(认证)
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        // 添加认证放行的用户信息并设置权限
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    /**
     * 校验密码编码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        // 无需编码器进行匹配密码
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // //开启登录配置
                .antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
                .antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }
}

4.1.6 测试

LoginController类的内容:

package com.king.security.securityspringboot.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 登录控制层
 **/
@RestController
public class LoginController {
    /**
     * 认证成功将跳转到/login-success
     * @return
     */
    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        return " 登录成功";
    }

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(){
        return " 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    public String r2(){
        return " 访问资源2";
    }
}

测试过程:
1、测试认证
在这里插入图片描述
在这里插入图片描述
2、测试退出
在这里插入图片描述
3、测试授权
在这里插入图片描述
在这里插入图片描述

4.2 工作原理

4.2.1 结构总览

        Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过 FilterAOP 等技术来实现,Spring SecurityWeb 资源的保护是靠 Filter 实现的,所以从这个 Filter 来入手,逐步深入 Spring Security 原理。

        当初始化 Spring Security时,会创建一个名为 SpringSecurityFilterChainServlet过滤器 ,类型为 org.springframework.security.web.FilterChainProxy,它实现javax.servlet.Filter,因此外部的请求会经过此 类,下图是 Spring Security 过虑器链结构图:
在这里插入图片描述
        FilterChainProxy 是一个代理,真正起作用的是 FilterChainProxySecurityFilterChain 所包含的各个Filter ,同时这些 Filter 作为 BeanSpring 管理,它们是 Spring Security 核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)决策管理器 (AccessDecisionManager)进行处理,下图是 FilterChainProxy 相关类的 UML 图示。
在这里插入图片描述
Spring Security 功能的实现主要是由一系列过滤器链相互配合完成。

在这里插入图片描述
下面介绍过滤器链中主要的几个过滤器及其作用:

SecurityContextPersistenceFilter 这个 Filter 是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 SecurityContextHolder 所持有的SecurityContext;

UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandlerAuthenticationFailureHandler,这些都可以根据需求做相关改变;

FilterSecurityInterceptor 是用于保护 web 资源的,使用 AccessDecisionManager 对当前用户进行授权访问,前面已经详细介绍过了;

ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationExceptionAccessDeniedException,其它的异常它会继续抛出。

4.2.2.认证流程

4.2.2.1 认证流程

在这里插入图片描述
让我们仔细分析认证过程:

  1. 用户提交用户名、密码被 SecurityFilterChain 中的 UsernamePasswordAuthenticationFilter 过滤器获取到,封装为请求 Authentication,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
  2. 然后过滤器将 Authentication 提交至认证管理器(AuthenticationManager)进行认证。
  3. 认证成功后,AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
  4. SecurityContextHolder安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(...) 方法,设置到其中。可以看出AuthenticationManager 接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为 ProviderManager。而 Spring Security 支持多种认证方式,因此 ProviderManager 维护着一个 List<AuthenticationProvider> 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider 完成的。咱们知道 web 表单的对应的 AuthenticationProvider 实现类为 DaoAuthenticationProvider,它的内部又维护着一个 UserDetailsService 负责 UserDetails 的获取。最终 AuthenticationProviderUserDetails 填充至 Authentication

认证核心组件的大体关系如下:
在这里插入图片描述

4.3.2.2.AuthenticationProvider

通过前面的 Spring Security 认证流程我们得知,认证管理器(AuthenticationManager)委托 AuthenticationProvider 完成认证工作。

AuthenticationProvider 是一个接口,定义如下:

public interface AuthenticationProvider {
       Authentication authenticate(Authentication authentication) throws AuthenticationException;
       boolean supports(Class<?> var1);
}

authenticate() 方法定义了认证的实现过程,它的参数是一个Authentication,里面包含了登录用户所提交的用 户、密码等。而返回值也是一个 Authentication ,这个 Authentication 则是在认证成功后,将用户的权限及其他信息重新组装后生成。

Spring Security中维护着一个 List<AuthenticationProvider> 列表,存放多种认证方式,不同的认证方式使用不同的 AuthenticationProvider。如使用用户名密码登录时,使用 AuthenticationProvider1,短信登录时使用AuthenticationProvider2 等等这样的例子很多。

每个 AuthenticationProvider 需要实现 supports() 方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时 Spring Security 会生成UsernamePasswordAuthenticationToken,它是一个 Authentication,里面封装着用户提交的用户名、密码信息。而对应的,哪个AuthenticationProvider来处理它?

DaoAuthenticationProvider 的基类AbstractUserDetailsAuthenticationProvider 发现以下代码:

public boolean supports(Class<?> authentication) {
       return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

也就是说当 web 表单提交用户名密码时,Spring SecurityDaoAuthenticationProvider 处理。

最后,来看一下 Authentication(认证信息) 的结构,它是一个接口,我们之前提到的 UsernamePasswordAuthenticationToken 就是它的实现之一:

public interface Authentication extends Principal, Serializable { (1)

        Collection<? extends GrantedAuthority> getAuthorities(); (2)

        Object getCredentials(); (3)

        Object getDetails(); (4)

        Object getPrincipal(); (5)

        boolean isAuthenticated();

        void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

(1) Authenticationspring security 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个 getName() 方法。

(2) getAuthorities() 是获取权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是代表权限信息的一系列字符串。

(3) getCredentials() 凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。

(4) getDetails() 细节信息,web 应用中的实现接口通常为WebAuthenticationDetails,它记录了访问者的 ip 地址和 sessionId 的值。

(5) getPrincipal(),身份信息,大部分情况下返回的是 UserDetails 接口的实现类,UserDetails 代表用户的详细信息,那从 Authentication 中取出来的 UserDetails 就是当前登录用户信息,它也是框架中的常用接口之一。

4.3.2.3.UserDetailsService

1)认识UserDetailsService

现在咱们现在知道 DaoAuthenticationProvider 处理了 web 表单的认证逻辑,认证成功后既得到一个 Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal)。这个身份 信息就是一个 Object ,大多数情况下它可以被强转为 UserDetails 对象。

public interface UserDetailsService {
       UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

很多人把 DaoAuthenticationProviderUserDetailsService 的职责搞混淆,其实 UserDetailsService 只负责从特定 的地方(通常是数据库)加载用户信息,仅此而已。而 DaoAuthenticationProvider 的职责更大,它完成完整的认证流程,同时会把 UserDetails 填充至 Authentication

上面一直提到 UserDetails 是用户信息,咱们看一下它的真面目:

public interface UserDetails extends Serializable {
      Collection<? extends GrantedAuthority> getAuthorities();
      String getPassword();
      String getUsername();
      boolean isAccountNonExpired();
      boolean isAccountNonLocked();
      boolean isCredentialsNonExpired();
      boolean isEnabled();
}

它和 Authentication 接口很类似,比如它们都拥有usernameauthoritiesAuthenticationgetCredentials()UserDetails 中的 getPassword() 需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。Authentication 中的 getAuthorities() 实际是由UserDetails的getAuthorities() 传递而形成的。还记得 Authentication 接口中的getDetails() 方法吗?其中的 UserDetails 用户详细信息便是经过了 AuthenticationProvider 认证之后被填充的。

通过实现 UserDetailsServiceUserDetails ,我们可以完成对用户信息获取方式以及用户信息字段的扩展。

Spring Security 提供的 InMemoryUserDetailsManager(内存认证)JdbcUserDetailsManager(jdbc认证) 就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。

2)测试

自定义UserDetailsService

package com.king.security.securityspringboot.service;

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;

/**
 * 自定UserDetails用户认证器
 */
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    /**
     * 测试此方法要把com.king.security.securityspringboot.config.userDetailsService()注释掉
     * 根据用户账号查询信息
     * @param username 用户账号
     * @return 返回用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //登录账号
        System.out.println("username="+username);
        //根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
        UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
        return userDetails;
    }
}

屏蔽 WebSecurityConfig 安全配置类中 UserDetailsService() 方法的定义:

/**
 * spring security安全配置
 **/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 定义用户信息认证服务(认证)
     * @return
     */
    // @Bean
    // public UserDetailsService userDetailsService(){
    //     InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    //     // 添加认证放行的用户信息并设置权限
    //     manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
    //     manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
    //     return manager;
    // }

	/**
     * 密码编码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        // 无需编码器进行匹配密码
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // //开启登录配置
                .antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
                .antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }
}    

重启工程,请求认证,SpringDataUserDetailsServiceloadUserByUsername 方法被调用 ,查询用户信息。

4.3.2.4.PasswordEncoder

1)认识PasswordEncoder

DaoAuthenticationProvider 认证处理器通过 UserDetailsService 获取到UserDetails 后,它是如何与请求 Authentication 中的密码做对比呢?

在这里 Spring Security 为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider 通过 PasswordEncoder 接口的 matches 方法进行密码的对比,而具体的密码对比细节取决于实现:

public interface PasswordEncoder {
        String encode(CharSequence var1);

        boolean matches(CharSequence var1, String var2);

        default boolean upgradeEncoding(String encodedPassword) {
            return false;
        }
}

Spring Security 提供很多内置的 PasswordEncoder,能够开箱即用,使用某种 PasswordEncoder 只需要进行如下声明即可,如下:

@Bean
public PasswordEncoder passwordEncoder() {
      return  NoOpPasswordEncoder.getInstance();
}

NoOpPasswordEncoder 采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:

  1. 用户输入密码(明文 )
  2. DaoAuthenticationProvider 获取 UserDetails (其中存储了用户的正确密码)
  3. DaoAuthenticationProvider 使用 PasswordEncoder 对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。

NoOpPasswordEncoder 的校验规则拿输入的密码和 UserDetails 中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则校验失败。

实际项目中推荐使用 BCryptPasswordEncoder , Pbkdf2PasswordEncoder , SCryptPasswordEncoder 等,感兴趣的大家可以看看这些 PasswordEncoder 的具体实现。

2)使用BCryptPasswordEncoder

1、配置 BCryptPasswordEncoder 在安全配置类中定义:

@Bean
public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
}

测试发现认证失败,提示:Encoded password does not look like BCrypt

原因:
        由于 UserDetails 中存储的是原始密码(比如:123),它不是 BCrypt 格式。

        跟踪 DaoAuthenticationProvider33行代码查看 userDetails 中的内容 ,跟踪第38 行代码查看 PasswordEncoder 的类型。

2、测试BCrypt

通过下边的代码测试 BCrypt 加密及校验的方法

添加依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring‐boot‐starter‐test</artifactId>
	<scope>test</scope>
</dependency>

编写测试方法:

package com.king.security.securityspringboot.testbcrypt;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
public class TestBCrypt {

    @Test
    public void test1() {
        // 使用BCrypt给密码加密,第一个参数:原始密码;第二个参数:加盐,BCrypt.gensalt():自动生成盐,每次生成都不一样
        String hashpw = BCrypt.hashpw("123", BCrypt.gensalt());
        System.out.println("------------------使用BCrypt加密后的密码:" + hashpw);

        // 校验密码,第一个参数:明文密码;第二个参数:明文密码使用BCrypt方式加密后的密码
        boolean checkpw = BCrypt.checkpw("123","$2a$10$IPjZxNeVstgKQh0P2X9DX.IXaMQrSFiDQg0wr8DVffqyAK3.bwggy");
        boolean checkpw2 = BCrypt.checkpw("123","$2a$10$DRhG8b6Sv8oQpBaYuOM.UO83EP8XOaJNFMsnQ69n3J4jT5zB8twZm");
        System.out.println(checkpw); // true
        System.out.println(checkpw2); // true
    }
}

3、修改安全配置类

UserDetailsService() 方法中的原始密码修改为 BCrypt 格式:

package com.king.security.securityspringboot.config;

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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * spring security安全配置
 **/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 定义用户信息认证服务(认证)
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        // 添加认证放行的用户信息并设置权限
        manager.createUser(User.withUsername("zhangsan").password("$2a$10$IPjZxNeVstgKQh0P2X9DX.IXaMQrSFiDQg0wr8DVffqyAK3.bwggy").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    /**
     * 检验密码编码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        // 无需编码器进行匹配密码
        // return NoOpPasswordEncoder.getInstance();
        
        // 使用BCrypt方式进行匹配密码
        return new BCryptPasswordEncoder();
    }

    /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // //开启登录配置
                .antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
                .antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }



}

实际项目中存储在数据库中的密码并不是原始密码,都是经过加密处理的密码。

4.2.3.授权流程

4.2.3.1 授权流程

        通过快速上手我们知道,Spring Security 可以通过 http.authorizeRequests()web 请求进行授权保护。Spring Security 使用 标准Filter 建立了对 web 请求的拦截,最终实现对资源的授权访问。

Spring Security 的授权流程如下:
在这里插入图片描述
分析授权流程:

  1. 拦截请求,已认证用户访问受保护的 web 资源将被 SecurityFilterChain 中的 FilterSecurityInterceptor 的子类拦截。
  2. 获取资源访问策略,FilterSecurityInterceptor 会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection<ConfigAttribute>
    SecurityMetadataSource 其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:
  http
     .authorizeRequests()
        .antMatchers("/r/r1").hasAuthority("p1")
        .antMatchers("/r/r2").hasAuthority("p2")
        ...
  1. 最后,FilterSecurityInterceptor 会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。

AccessDecisionManager(访问决策管理器)的核心接口如下:

public interface AccessDecisionManager {
        /**
         * 通过传递的参数来决定用户是否有访问对应受保护资源的权限
         */
        void decide(Authentication authentication, Object object, Collection<ConfigAttribute>
                configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; //略..
}

这里着重说明一下 decide 的参数:

authentication : 要访问资源的访问者的身份。

object : 要访问的受保护资源,web 请求对应 FilterInvocation

configAttributes : 是受保护资源的访问策略,通过 SecurityMetadataSource 获取。

decide 接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

4.2.3.2 授权决策

AccessDecisionManager 采用投票的方式来确定是否能够访问受保护资源。
在这里插入图片描述

通过上图可以看出,AccessDecisionManager 中包含的一系列 AccessDecisionVoter 将会被用来对Authentication 是否有权访问受保护对象进行投票,AccessDecisionManager 根据投票结果,做出最终决策。

AccessDecisionVoter 是一个接口,其中定义有三个方法,具体结构如下所示:

public interface AccessDecisionVoter<S> {

        int ACCESS_GRANTED = 1;
        int ACCESS_ABSTAIN = 0;
        int ACCESS_DENIED =1;

        boolean supports(ConfigAttribute var1);

        boolean supports(Class<?> var1);

        int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}

vote() 方法的返回结果会是 AccessDecisionVoter 中定义的三个常量之一。ACCESS_GRANTED 表示同意, ACCESS_DENIED 表示拒绝,ACCESS_ABSTAIN 表示弃权。如果一个AccessDecisionVoter 不能判定当前 Authentication 是否拥有访问对应受保护对象的权限,则其vote() 方法的返回值应当为弃权 ACCESS_ABSTAIN

Spring Security 内置了三个基于投票的 AccessDecisionManager 实现类如下,它们分别是 AffirmativeBasedConsensusBasedUnanimousBased

AffirmativeBased 的逻辑是:
(1)只要有 AccessDecisionVoter 的投票为 ACCESS_GRANTED 则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出 AccessDeniedException
Spring Security默认使用的是 AffirmativeBased

ConsensusBased 的逻辑是:
(1)如果赞成票多于反对票则表示通过;
(2)反过来,如果反对票多于赞成票则将抛出 AccessDeniedException ;
(3)如果赞成票与反对票相同且不等于 0 ,并且属性 allowIfEqualGrantedDeniedDecisions 的值为 true ,则表示通过,否则将抛出异常 AccessDeniedException 。参数 allowIfEqualGrantedDeniedDecisions 的值默认为 true ;
(4)如果所有的 AccessDecisionVoter 都弃权了,则将视参数 allowIfAllAbstainDecisions 的值而定,如果该值为 true 则表示通过,否则将抛出异常 AccessDeniedException 。参数 allowIfAllAbstainDecisions 的值默认为 false

UnanimousBased 的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给 AccessDecisionVoter 进行投票,而 UnanimousBased 会一次只传递一个 ConfigAttributeAccessDecisionVoter 进行投票。这也就意味着如果我们的AccessDecisionVoter 的逻辑是只要传递进来的 ConfigAttribute 中有一个能够匹配则投赞成票,但是放到 UnanimousBased 中其投票结果就不一定是赞成了。 UnanimousBased 的逻辑具体来说是这样的:
(1)如果受保护对象配置的某一个 ConfigAttribute 被任意的 AccessDecisionVoter 反对了,则将抛出 AccessDeniedException ;
(2)如果没有反对票,但是有赞成票,则表示通过;
(3)如果全部弃权了,则将视参数 allowIfAllAbstainDecisions 的值而定,true 则通过,false 则抛出 AccessDeniedException

Spring Security 也内置一些投票者实现类如 RoleVoterAuthenticatedVoterWebExpressionVoter 等,可以自行查阅资料进行学习。

4.3 自定义认证

        Spring Security 提供了非常好的认证扩展方法,比如:快速上手中将用户信息存储到内存中,实际开发中用户信息通常在数据库,Spring security 可以实现从数据库读取用户信息,Spring security 还支持多种授权方法。

4.3.1 自定义登录页面

        在 Spring Security 快速上手 项目中,你可能会想知道登录页面从哪里来的?因为我们并没有提供任何的 HTMLJSP 文件。Spring Security 的默认配置没有明确设定一个登录页面的 URL ,因此Spring Security 会根据启用的功能自动生成一个登录页面 URL ,并使用默认 URL 处理登录的提交内容,登录后跳转的到默认 URL 等等。尽管自动生成的登录页面很方便快速启动和运行,但大多数应用程序都希望定义自己的登录页面。

4.3.1.1 认证页面

security-springmvc工程login.jsp 拷贝到 security-springboot工程 下,目录保持一致。
在这里插入图片描述

4.3.1.2 配置认证页面

WebConfig.java 中配置认证页面地址:

package com.king.security.securityspringboot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

/**
 * ServletContext,就相当于springmvc.xml文件
 **/
@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * 配置页面映射列表
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 默认Url根路径跳转到/login,此url为spring security提供的登录页
        // addViewController:URL访问路径
        // setViewName:映射的页面名称
        // 重定向到Spring Security自带的/login页面去
        // registry.addViewController("/").setViewName("redirect:/login");

        // 转向到自定义login页面去
        registry.addViewController("/").setViewName("redirect:/loginView");
        registry.addViewController("/loginView").setViewName("login");
    }

    /**
     * 配置拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 这里使用SpringSecurity做认证、授权、拦截功能,就不用自定义拦截器了
    }

    /**
     * 配置跨域
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
                .allowCredentials(true) // 允许带cookie访问
                .allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
                .allowedHeaders("token")
                .maxAge(3600);
    }
}
4.3.1.3 修改Spring Security安全配置文件

WebSecurityConfig 文件中配置表单登录信息:

package com.king.security.securityspringboot.config;

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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * spring security安全配置
 **/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 定义用户信息认证服务(认证)
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        // 添加认证放行的用户信息并设置权限
        manager.createUser(User.withUsername("zhangsan").password("$2a$10$IPjZxNeVstgKQh0P2X9DX.IXaMQrSFiDQg0wr8DVffqyAK3.bwggy").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    /**
     * 检验密码编码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        // 无需编码器进行匹配密码
        // return NoOpPasswordEncoder.getInstance();

        // 使用BCrypt方式进行匹配密码
        return new BCryptPasswordEncoder();
    }

    /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // //开启登录配置
                .antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
                .antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录 (1)
                .loginPage("/loginView") // 登录页请求路径 (2)
                .loginProcessingUrl("/login") // 指定登录处理的URL,也就是用户名、密码表单提交的目的路径 (3)
                .successForwardUrl("/login-success");//自定义登录成功的页面地址 (4)

    }

}

(1)允许表单登录;
(2)指定我们自己的登录页,spring security 以重定向方式跳转到 /loginView URL ,这个URL 映射登录页;
(3)指定登录处理的 URL ,也就是用户名、密码表单提交的目的路径;
(4)指定登录成功后的跳转 URL
(5)我们必须允许所有用户访问我们的登录页(例如为验证的用户),这个 formLogin().permitAll() 方法允许任意用户访问基于表单登录的所有的 URL

4.3.1.4 测试

当用户没有认证时访问系统的资源会重定向到 login 页面:
在这里插入图片描述
输入账号和密码,点击登录,报错:
在这里插入图片描述
问题解决:
spring security 为防止 CSRF(Cross-site request forgery跨站请求伪造) 的发生,限制了除了 get 以外的大多数方法。

解决方法1:
屏蔽 CSRF 控制,即 spring security 不再限制 CSRF
修改配置 WebSecurityConfig文件:

   /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() // csrf().disable():屏蔽CSRF控制,即spring security不再限制CSRF
                .authorizeRequests()  //开启登录配置
                .antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
                .antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .loginPage("/loginView") // 登录页请求路径
                .loginProcessingUrl("/login") // 指定登录处理的URL,也就是用户名、密码表单提交的目的路径
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }

本案例采用方法1。

解决方法2:
login.jsp 页面添加一个 tokenspring security 会验证 token ,如果 token 合法则可以继续请求。
修改 login.jsp 文件:

<form action="login" method="post">
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
	... 
</form>

在这里插入图片描述

4.3.2 连接数据库认证

        前面的例子是将用户信息存储在内存中,实际项目中用户信息存储在数据库中,现在实现从数据库读取用户信息。根据前边对认证流程研究,只需要重新定义 UserDetailService 即可实现根据用户账号查询数据库。

4.3.2.1 创建数据库

创建 user_db 数据库:

CREATE DATABASE `user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

创建 t_user 表:

CREATE TABLE `t_user` (
	`id` BIGINT ( 20 ) NOT NULL COMMENT '用户id',
	`username` VARCHAR ( 64 ) NOT NULL,
	`password` VARCHAR ( 64 ) NOT NULL,
	`fullname` VARCHAR ( 255 ) NOT NULL COMMENT '用户姓名',
	`mobile` VARCHAR ( 11 ) DEFAULT NULL COMMENT '手机号',
PRIMARY KEY ( `id` ) USING BTREE 
) ENGINE = INNODB DEFAULT CHARSET = utf8 ROW_FORMAT = DYNAMIC
4.3.2.2 代码实现

1)定义 dataSource
application.properties 配置文件:

# 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=rootroot

2)添加依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.47</version>
</dependency>

3)定义 Dao
定义模型类型,在 model 包定义 UserDto :

@Data
  public class UserDto {
      private String id;
      private String username;
      private String password;
      private String fullname;
      private String mobile;
}

Dao包定义 UserDao:

package com.king.security.securityspringboot.dao;

import com.king.security.securityspringboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class UserDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    /**
     * 根据用户名查询用户信息
     * @param username
     * @return
     */
    public UserDto getUserByUsername(String username){

        String sql ="select id,username,password,fullname from t_user where username = ?";

        List<UserDto> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));

        if(list == null || list.size() <= 0){
            return null;
        }

        return list.get(0);
    }

}
4.3.2.3 定义UserDetailService

service 包下定义 SpringDataUserDetailsService 类,实现 UserDetailsService 接口进行用户验证 :

package com.king.security.securityspringboot.service;

import com.king.security.securityspringboot.dao.UserDao;
import com.king.security.securityspringboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * 自定UserDetails用户认证器
 */
// 注释不用,让它不加入Spring容器中
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    /**
     * 测试此方法要把com.king.security.securityspringboot.config.userDetailsService()注释掉
     * 根据用户账号查询信息
     * @param username 用户账号
     * @return 返回用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //登录账号
        System.out.println("username="+username);

        //根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
        // UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
        // return userDetails;

        //根据账号去数据库查询...
        UserDto userDto = userDao.getUserByUsername(username);

        if(userDto == null){
            return null;
        }

        // 这里的权限暂时使用静态数据,用户信息是动态数据
        UserDetails userDetails = User.withUsername(userDto.getFullname()).password(userDto.getPassword()).authorities("p1").build();
        return userDetails;
    }
}
4.3.2.4 测试

输入账号和密码请求认证,跟踪代码。

4.3.2.5 使用BCryptPasswordEncoder

按照前边讲的 PasswordEncoder 的使用方法,使用 BCryptPasswordEncoder 需要完成如下工作:

1、在 WebSecurityConfig 安全配置类中定义 BCryptPasswordEncoder :

   /**
     * 检验密码编码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        // 无需编码器进行匹配密码
        // return NoOpPasswordEncoder.getInstance();

        // 使用BCrypt方式进行匹配密码
        return new BCryptPasswordEncoder();
    }

2、UserDetails 中的密码存储 BCrypt 格式
前边实现了从数据库查询用户信息,所以数据库中的密码应该存储 BCrypt 格式:
在这里插入图片描述

4.4 会话

        用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security 提供会话管理,认证通过后将身份信息放入 SecurityContextHolder 上下文,SecurityContext 与当前线程进行绑定,方便获取 用户身份。

4.4.1.获取用户身份

编写 LoginController 类,实现 /r/r1/r/r2 的测试资源,并修改 login-success 方法,注意 getUsername 方法,Spring Security 获取当前登录用户信息的方法为 SecurityContextHolder.getContext().getAuthentication() :

package com.king.security.securityspringboot.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 登录控制层
 **/
@RestController
public class LoginController {

    /**
     * 认证成功将跳转到/login-success
     * @return
     */
    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){

        String username = getUsername();
        return username + ": 登录成功";
    }

    /**
     * 获取当前登录用户名
     */
    private String getUsername(){
        // Spring Security获取当前登录用户认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // 判断没有认证就返回null
        if(!authentication.isAuthenticated()){
            return null;
        }

        // 获取用户身份
        Object principal = authentication.getPrincipal();

        String username = null;

        // 判断 principal 是否 是UserDetails对象
        if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
            // 将principal对象强转UserDetails对象拿用户名
            username = ((org.springframework.security.core.userdetails.UserDetails)principal).getUsername();
        } else {
            username = principal.toString();
        }
        return username;
    }

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(){
        String username = getUsername();
        return username + " 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    public String r2(){
        String username = getUsername();
        return username + " 访问资源2";
    }
}

测试
登录前访问 http://localhost:8080/security-springboot/r/r1 资源。
被重定向至登录页面:
在这里插入图片描述

登录后访问资源。
成功访问资源,如下:
在这里插入图片描述
在这里插入图片描述

4.4.2会话控制

可以通过以下选项准确控制会话何时创建以及 Spring Security 如何与之交互:

机制描述
always如果没有session存在就创建一个
ifRequired如果需要就创建一个Session(默认)登录时
neverSpringSecurity 将不会创建Session,但是如果应用中其他地方创建了Session,那么Spring Security将会使用它。
statelessSpringSecurity将绝对不会创建Session,也不使用Session

通过 WebSecurityConfig 文件以下配置方式对该选项进行配置:

   @Override
   protected void configure(HttpSecurity http) throws Exception {
      http.sessionManagement()
           .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
   }

默认情况下,Spring Security 会为每个登录成功的用户会新建一个session ,就是 ifRequired

若选用 never ,则指示 Spring Security 对登录成功的用户不创建 session 了,但若你的应用程序在某地方新建了 session,那么 Spring Security 会用它的。

若使用 stateless ,则说明 Spring Security 对登录成功的用户不会创建 session 了,你的应用程序也不会允许新建 session 。并且它会暗示不使用 cookie ,所以每个请求都需要重新进行身份验证。这种无状态架构适用于 REST API 及其无状态认证机制。

4.4.2.1会话超时

可以再 sevlet 容器中设置 session 的超时时间,如下设置 session 有效期为 3600s ;

spring boot 配置文件:

server.servlet.session.timeout=3600s

session 超时之后,可以通过 Spring Security 设置跳转的路径。

http.sessionManagement()
   .expiredUrl("/login‐view?error=EXPIRED_SESSION")
   .invalidSessionUrl("/login‐view?error=INVALID_SESSION");

expiredsession 过期,invalidSession 指传入的 sessionid 无效。

4.4.2.2 安全会话cookie

我们可以使用 httpOnlysecure 标签来保护我们的会话 cookie :

  • httpOnly :如果为 true ,那么浏览器脚本将无法访问 cookie
  • secure :如果为 true ,则 cookie 将仅通过 HTTPS 连接发送

spring boot 配置文件:

server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true

4.6 退出

Spring security 默认实现了 logout 退出,访问 /logout ,果然不出所料,退出功能 Spring 也替我们做好了。
在这里插入图片描述
点击 “Log Out”退出成功。
退出 后访问其它 url 判断是否成功退出。

这里也可以自定义退出成功的页面:
WebSecurityConfig 文件的 protected void configure(HttpSecurity http) 方法中配置:

		.and()
        .logout() // 自定义退出
        .logoutUrl("/logout") // 退出URL
        .logoutSuccessUrl("/loginView?logout"); //  退出成功后的页面

当退出操作出发时,将发生:

  • 使 HTTP Session 无效
  • 清除 SecurityContextHolder 跳转到 /loginView?logout

但是,类似于配置登录功能,咱们可以进一步自定义退出功能:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests() //开启登录配置
                //...
                .and()
                .logout()                                           (1)
                .logoutUrl("/logout")                               (2)
                .logoutSuccessUrl("/login‐view?logout")             (3)
                .logoutSuccessHandler(logoutSuccessHandler)         (4)
                .addLogoutHandler(logoutHandler)                    (5)
                .invalidateHttpSession(true);                       (6)
    }

(1)提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用;
(2)设置触发退出操作的URL (默认是 /logout );
(3)退出之后跳转的 URL 。默认是 /login?logout;
(4)定制的 LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么 logoutSuccessUrl() 的设置会被忽略;
(5)添加一个 LogoutHandler ,用于实现用户退出时的清理工作,默认SecurityContextLogoutHandler 会被添加为最后一个 LogoutHandler ;
(6)指定是否在退出时让 HttpSession 无效。 默认设置为 true

注意:如果让 logoutGET 请求下生效,必须关闭防止 CSRF 攻击 csrf().disable() 。如果开启了 CSRF ,必须使用 post 方式请求 /logout

logoutHandler
一般来说, LogoutHandler 的实现类被用来执行必要的清理,因而它们不应该抛出异常。 下面是 Spring Security 提供的一些实现:

  • PersistentTokenBasedRememberMeServices 基于持久化 tokenRememberMe 功能的相关清理
  • TokenBasedRememberMeService 基于 tokenRememberMe 功能的相关清理
  • CookieClearingLogoutHandler 退出时 Cookie 的相关清理
  • CsrfLogoutHandler 负责在退出时移除 csrfToken
  • SecurityContextLogoutHandler 退出时 SecurityContext 的相关清理

链式 API 提供了调用相应的 LogoutHandler 实现的快捷方式,比如 deleteCookies()

4.7 授权

4.7.1 概述

        授权的方式包括 web 授权和方法授权web 授权是通过 url 拦截进行授权,方法授权通过方法拦截进行授权。它们都会调用accessDecisionManager 进行授权决策,若为 web 授权则拦截器为 FilterSecurityInterceptor ;若为方法授权则拦截器为 MethodSecurityInterceptor 。如果同时通过 web 授权和方法授权则先执行web 授权,再执行方法授权,最后决策通过,则允许访问资源,否则将禁止访问。

类关系如下:
在这里插入图片描述

4.7.2.准备环境

4.7.2.1 数据库环境

t_user 表的 user_db 数据库创建如下表:

t_role 角色表:

CREATE TABLE `t_role` (
	`id` VARCHAR ( 32 ) NOT NULL,
	`role_name` VARCHAR ( 255 ) DEFAULT NULL,
	`description` VARCHAR ( 255 ) DEFAULT NULL,
	`create_time` datetime DEFAULT NULL,
	`update_time` datetime DEFAULT NULL,
	`status` CHAR ( 1 ) NOT NULL,
	PRIMARY KEY ( `id` ),
	UNIQUE KEY `unique_role_name` ( `role_name` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8 

insert into `t_role`(`id`,`role_name`,`description`,`create_time`,`update_time`,`status`) values ('1','管理员',NULL,NULL,NULL,'');

t_user_role 用户角色关系表:

CREATE TABLE `t_user_role` (
	`user_id` VARCHAR ( 32 ) NOT NULL,
	`role_id` VARCHAR ( 32 ) NOT NULL,
	`create_time` datetime DEFAULT NULL,
	`creator` VARCHAR ( 255 ) DEFAULT NULL,
	PRIMARY KEY ( `user_id`, `role_id` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8

insert  into `t_user_role`(`user_id`,`role_id`,`create_time`,`creator`) values ('1','1',NULL,NULL);

t_permission 权限表:

CREATE TABLE `t_permission` (
	`id` VARCHAR ( 32 ) NOT NULL,
	`code` VARCHAR ( 32 ) NOT NULL COMMENT '权限标识符',
	`description` VARCHAR ( 64 ) DEFAULT NULL COMMENT '描述',
	`url` VARCHAR ( 128 ) DEFAULT NULL COMMENT '请求地址',
	PRIMARY KEY ( `id` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8

insert into `t_permission`(`id`,`code`,`description`,`url`) values ('1','p1','测试资源 1','/r/r1'),('2','p3','测试资源2','/r/r2');

t_role_permission 角色权限关系表:

CREATE TABLE `t_role_permission` (
	`role_id` VARCHAR ( 32 ) NOT NULL,
	`permission_id` VARCHAR ( 32 ) NOT NULL,
	PRIMARY KEY ( `role_id`, `permission_id` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8

insert  into `t_role_permission`(`role_id`,`permission_id`) values ('1','1'),('1','2');

在这里插入图片描述

4.7.2.2 修改UserDetailService

1、修改 dao 接口
定义模型类型,在 model 包定义 PermissionsDto :

package com.king.security.securityspringboot.model;

import lombok.Data;

@Data
public class PermissionsDto {

    private String id;
    private String code;
    private String description;
    private String url;
}

Dao包的 UserDao 文件新增 findPermissionsByUserId() 方法:

package com.king.security.securityspringboot.dao;

import com.king.security.securityspringboot.model.PermissionsDto;
import com.king.security.securityspringboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

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

@Repository
public class UserDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    /**
     * 根据用户名查询用户信息
     * @param username
     * @return
     */
    public UserDto getUserByUsername(String username){

        String sql ="select id,username,password,fullname from t_user where username = ?";

        List<UserDto> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));

        if(list == null || list.size() <= 0){
            return null;
        }

        return list.get(0);
    }

    /**
     * 根据用户id查询用户权限
     * @param userId
     * @return
     */
    public List<String> findPermissionsByUserId(String userId) {
        
        String sql = "SELECT * FROM t_permission WHERE id IN( " +
                "SELECT t_role_permission.permission_id FROM t_role_permission WHERE t_role_permission.role_id IN ( " +
                "SELECT t_user_role.role_id FROM t_user_role WHERE t_user_role.user_id = ? ) )";

        List<PermissionsDto> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionsDto.class));

        List<String> permissionsList = new ArrayList<>();

        list.forEach(permissionsDto -> permissionsList.add(permissionsDto.getCode()));
        
        return permissionsList;
    }

}

2、修改 UserDetailService
实现从数据库读取权限:

package com.king.security.securityspringboot.service;

import com.king.security.securityspringboot.dao.UserDao;
import com.king.security.securityspringboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
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.List;

/**
 * 自定UserDetails用户认证器
 */
// 注释不用,让它不加入Spring容器中
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    /**
     * 测试此方法要把com.king.security.securityspringboot.config.userDetailsService()注释掉
     * 根据用户账号查询信息
     * @param username 用户账号
     * @return 返回用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //登录账号
        System.out.println("username="+username);

        //根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
        // UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
        // return userDetails;

        //根据账号去数据库查询...
        UserDto userDto = userDao.getUserByUsername(username);

        if(userDto == null){
            return null;
        }

        //查询用户权限
        List<String> permissionsList = userDao.findPermissionsByUserId(userDto.getId());

        // 定义一个数据
        String[] permissionsArray = new String[permissionsList.size()];

        // 将 permissionsList 转成 数组
        permissionsList.toArray(permissionsArray);

        //创建userDetails
        UserDetails userDetails = User.withUsername(userDto.getFullname()).password(userDto.getPassword()).authorities(permissionsArray).build();
        return userDetails;
    }
}

测试:
在这里插入图片描述
在这里插入图片描述

4.7.3.web授权

        在上面例子中完成了认证拦截,并对 /r/** 下的某些资源进行简单的授权保护,但是想进行灵活的授权控制该怎么做呢?通过给 http.authorizeRequests() 添加多个子节点来定制需求到程序中的 URL,如下代码:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()                                                        (1)
            .antMatchers("/r/r1").hasAuthority("p1")                                    (2)
            .antMatchers("/r/r2").hasAuthority("p2")                                    (3)
            .antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')")   (4)
            .antMatchers("/r/**").authenticated()                                       (5)
            .anyRequest().permitAll()                                                   (6)
            .and()
            .formLogin() // 允许表单登录
    // ...
}

(1) http.authorizeRequests() 方法有多个子节点,每个 macher 按照他们的声明顺序执行。
(2)指定"/r/r1" URL ,拥有 p1 权限能够访问
(3)指定"/r/r2" URL,拥有 p2 权限能够访问
(4)指定了 "/r/r3" URL,同时拥有 p1p2 权限才能够访问
(5)指定了除了 r1r2r3 之外 "/r/**" 资源,同时通过身份认证就能够访问,这里使用 SpEL(Spring Expression Language) 表达式
(6)剩余的尚未匹配的资源,不做保护。

注意:
规则的顺序是重要的,更具体的规则应该先写。现在以 /admin 开始的所有内容都需要具有 ADMIN角色 的身份验证用户,即使是 /admin/login路径 (因为/admin/login已经被 /admin /**规则匹配,因此第二个规则被忽略)

.antMatchers("/admin/**").hasRole(“ADMIN”)
.antMatchers("/admin/login").permitAll()

因此,登录页面的规则应该在 /admin/ ** 规则之前,例如:

.antMatchers("/admin/login").permitAll()
.antMatchers("/admin/**").hasRole(“ADMIN”)

保护 URL 常用的方法有:
authenticated() 保护 URL,需要用户登录。

permitAll() 指定 URL 无需保护,一般应用与静态资源文件。

hasRole(String role) 限制单个角色访问,角色将被增加 “ROLE_” 所以 ”ADMIN” 将和 “ROLE_ADMIN” 进行比较。

hasAuthority(String authority) 限制单个权限访问。

hasAnyRole(String... roles) 允许多个角色访问。

hasAnyAuthority(String... authorities) 允许多个权限访问。

access(String attribute) 该方法使用 SpEL表达式, 所以可以创建复杂的限制。

hasIpAddress(String ipaddressExpression) 限制IP地址或子网。

推荐基于资源授权,而不是角色授权!

4.7.4.方法授权(注解方式授权)

        现在我们已经掌握了使用如何使用 http.authorizeRequests()web资源 进行授权保护,从 Spring Security2.0版 本 开始,它支持服务层方法的安全性的支持。本节学习 @PreAuthorize , @PostAuthorize , @Secured 三类注解。可以在任何 @Configuration 实例上使用 @EnableGlobalMethodSecurity 注释来启用基于注解的安全性。

@EnableGlobalMethodSecurity 三方法详解:https://www.jianshu.com/p/77b4835b6e8e

以下内容将启用 Spring Security@Secured 注解
使用前提,Spring Security 默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter 的类上加 @EnableGlobalMethodSecurity( securedEnabled = true) 开启注解:

/**
 * spring security安全配置
 **/
@Configuration
@EnableGlobalMethodSecurity( securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
		......
}

通过注解形式给控制层的方法设置权限授权:

package com.king.security.securityspringboot.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 登录控制层
 **/
@RestController
public class LoginController {

    /**
     * 认证成功将跳转到/login-success
     * @return
     */
    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){

        String username = getUsername();
        return username + ": 登录成功";
    }

    /**
     * 获取当前登录用户名
     */
    private String getUsername(){
        // Spring Security获取当前登录用户认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // 判断没有认证就返回null
        if(!authentication.isAuthenticated()){
            return null;
        }

        // 获取用户身份
        Object principal = authentication.getPrincipal();

        String username = null;

        // 判断 principal 是否 是UserDetails对象
        if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
            // 将principal对象强转UserDetails对象拿用户名
            username = ((org.springframework.security.core.userdetails.UserDetails)principal).getUsername();
        } else {
            username = principal.toString();
        }
        return username;
    }

    /**
     * 测试资源1
     * @return
     */
    //@PreAuthorize("hasAuthority('p1')") // 拥有 p1 权限才可以访问
    //@PreAuthorize("hasAnyAuthority('p1','p3')") // 拥有 p1,p3 权限都可以访问
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(){
        String username = getUsername();
        return username + " 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    //@PreAuthorize("hasAuthority('p2')") // 拥有 p2 权限才可以访问
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    public String r2(){
        String username = getUsername();
        return username + " 访问资源2";
    }
}

然后向方法(在类或接口上)添加注解就会限制对该方法的访问。 Spring Security 的原生注释支持为该方法定义了 一组属性。 这些将被传递给 AccessDecisionManager 以供它作出实际的决定:

public interface BankService {

   @Secured("IS_AUTHENTICATED_ANONYMOUSLY")
   public Account readAccount(Long id);
   
   @Secured("IS_AUTHENTICATED_ANONYMOUSLY")
   public Account[] findAccounts();
   
   @Secured("ROLE_TELLER")
   public Account post(Account account, double amount);
}

以上配置标明 readAccount()findAccounts() 方法可匿名访问,底层使用 WebExpressionVoter 投票器,可从 AffirmativeBased 第23行代码跟踪。

post() 方法需要有 TELLER 角色才能访问,底层使用 RoleVoter 投票器。

使用如下代码可启用 prePost 注解的支持:

/**
 * spring security安全配置
 **/
@Configuration
@EnableGlobalMethodSecurity( prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
		......
}

相应 Java 代码如下:

public interface BankService {

   @PreAuthorize("isAnonymous()")
   public Account readAccount(Long id);
   
   @PreAuthorize("isAnonymous()")
   public Account[] findAccounts();
   
   @PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')")
   public Account post(Account account, double amount);
}

以上配置标明 readAccount()findAccounts() 方法可匿名访问,post() 方法需要同时拥有p_transferp_read_account 权限才能访问,底层使用 WebExpressionVoter 投票器,可从 AffirmativeBased 第23行代码跟踪。

5.分布式系统认证方案

5.1什么是分布式系统

        随着软件环境和需求的变化 ,软件的架构由单体结构演变为分布式架构,具有分布式架构的系统叫分布式系统,分布式系统的运行通常依赖网络,它将单体结构的系统分为若干服务,服务之间通过网络交互来完成用户的业务处理,当前流行的微服务架构就是分布式系统架构,如下图:
在这里插入图片描述
分布式系统具体如下基本特点:
1、分布性:每个部分都可以独立部署,服务之间交互通过网络进行通信,比如:订单服务、商品服务。

2、伸缩性:每个部分都可以集群方式部署,并可针对部分结点进行硬件及软件扩容,具有一定的伸缩能力。

3、共享性:每个部分都可以作为共享资源对外提供服务,多个部分可能有操作共享资源的情况。

4、开放性:每个部分根据需求都可以对外发布共享资源的访问接口,并可允许第三方系统访问。

5.2 分布式认证需求

        分布式系统的每个服务都会有认证、授权的需求,如果每个服务都实现一套认证授权逻辑会非常冗余,考虑分布式系统共享性的特点,需要由独立的认证服务处理系统认证授权的请求;考虑分布式系统开放性的特点,不仅对系统内部服务提供认证,对第三方系统也要提供认证。分布式认证的需求总结如下:

统一认证授权

提供独立的认证服务,统一处理认证授权。

无论是不同类型的用户,还是不同种类的客户端(web端,H5、APP),均采用一致的认证、权限、会话机制,实现统一认证授权。

要实现统一则认证方式必须可扩展,支持各种认证需求,比如:用户名密码认证、短信验证码、二维码、人脸识别等认证方式,并可以非常灵活的切换。

应用接入认证

应提供扩展和开放能力,提供安全的系统对接机制,并可开放部分API给接入第三方使用,一方应用(内部 系统服 务)和三方应用(第三方应用)均采用统一机制接入。

5.3 分布式认证方案

5.3.1 选型分析

1、基于 session 的认证方式
        在分布式的环境下,基于 session 的认证会出现一个问题,每个应用服务都需要在 session 中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将 session 信息带过去,否则会重新认证。
在这里插入图片描述
这个时候,通常的做法有下面几种:

Session复制:多台应用服务器之间同步 session ,使 session 保持一致,对外透明。

Session黏贴:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。

Session集中存储:将 Session 存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取Session。

总体来讲,基于 session 认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session 机制方式基于 cookie ,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高 session 的复制、黏贴及存储的容错性。

2、基于 token 的认证方式
        基于 token 的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把 token 存在任意地方,并且可以实现 webapp 统一认证机制。其缺点也很明显,token 由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token 的签名验签操作也会给 cpu 带来额外的处理负担。
在这里插入图片描述

5.3.2 技术方案

根据 选型的分析,决定采用基于 token 的认证方式,它的优点是:

1、适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。

2、token 认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议Oauth2.0JWT 等。

3、一般情况服务端无需存储会话信息,减轻了服务端的压力。

分布式系统认证技术方案见下图:
在这里插入图片描述
流程描述:
(1)用户通过接入方(应用)登录,接入方采取 OAuth2.0 方式在统一认证服务 (UAA) 中认证。
(2)认证服务 (UAA) 调用验证该用户的身份是否合法,并获取用户权限信息。
(3)认证服务 (UAA) 获取接入方权限信息,并验证接入方是否合法。
(4)若登录用户以及接入方都合法,认证服务生成 jwt 令牌返回给接入方,其中 jwt 中包含了用户权限及接入方权限。
(5)后续,接入方携带 jwt 令牌对 API 网关内的微服务资源进行访问。
(6) API 网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。
(7)如果接入方的权限没问题,API 网关将原请求 header 中附加解析后的明文 token ,并将请求转发至微服务。
(8)微服务收到请求,明文 token 中包含登录用户的身份和权限信息。因此后续微服务自己可以干两件事: 1,用户授权拦截(看当前用户是否有权访问该资源)。2 ,将用户信息存储进当前线程上下文(有利于后续业务逻辑随时 获取当前用户信息)。

流程所涉及到 UAA 服务、API 网关这三个组件职责如下:

1)统一认证服务(UAA)

它承载了 OAuth2.0 接入方认证、登入用户的认证、授权以及生成令牌的职责,完成实际的用户认证、授权功能。

2)API网关

作为系统的唯一入口,API 网关为接入方提供定制的 API 集合,它可能还具有其它职责,如身份验证、监控、负载均衡、缓存等。API 网关方式的核心要点是,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。

6. OAuth2.0

6.1 OAuth2.0介绍

         OAuth(开放授权) 是一个开放标准,允许用户授权第三方应用访问它们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享它们数据的所有内容。OAuth2.0OAuth协议 的延续版本,但不向后兼容OAuth 1.0 即完全废止了 OAuth1.0 。很多大公司如 GoogleYahooMicrosoft 等都提供了 OAuth认证服务 ,这些都足以说明 OAuth标准 逐渐成为开放资源授权的标准。

OAuth 协议目前发展到 2.0版本1.0版本 过于复杂,2.0版本 已得到广泛应用。

OAuth 协议:https://tools.ietf.org/html/rfc6749

下边分析一个 Oauth2认证 的例子,通过例子去理解 OAuth2.0协议 的认证流程,本例子是黑马程序员网站使用微信认证的过程,这个过程的简要描述如下:

         用户借助微信认证登录 知乎网站 ,用户就不用单独在 知乎网站 注册用户,怎么样算认证成功了? 知乎网站 需要成功从微信获取用户的身份信息则认为用户认证成功,那如何从微信获取用户的身份信息?用户信息的拥有者是用户本人,微信需要经过用户的同意方可为 知乎网站 生成令牌,知乎网站 拿此令牌方可从微信获取用户的信息。

1、客户端请求第三方授权
用户进入知乎网站的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。
在这里插入图片描述
点击“微信”出现一个二维码,此时用户扫描二维码,开始给 知乎网站 授权。
在这里插入图片描述
2、资源拥有者同意给客户端授权
资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,微信会询问用户是否给授权 知乎网站 访问自己的微信数据,用户点击 “同意” 表示同意授权,微信认证服务器会颁发一个授权码,并重定向到 知乎 的网站。
在这里插入图片描述

3、客户端获取到授权码,请求认证服务器申请令牌
此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。

4、认证服务器向客户端响应令牌
微信认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。 此交互过程用户看不到,当客户端拿到令牌后,用户在 知乎网站 看到已经登录成功。

5、客户端请求资源服务器的资源
客户端携带令牌访问资源服务器的资源。 知乎网站 携带令牌请求访问微信服务器获取用户的基本信息。

6、资源服务器返回受保护资源
资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。

以上认证授权详细的执行流程如下:
在这里插入图片描述
通过上边的例子,大概了解了 OAauth2.0 的认证过程,下边看 OAuth2.0 认证流程:
引自 OAauth2.0 协议 rfc6749 https://tools.ietf.org/html/rfc6749
在这里插入图片描述
Client:客户端;Resource Owner:资源拥有者;Authorization Server:授权服务器;Resource Server:资源服务器;

  • (A)用户打开客户端以后,客户端要求用户给予授权。
  • (B)用户同意给予客户端授权。
  • (C)客户端使用上一步获得的授权,向认证服务器申请令牌。
  • (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
  • (E)客户端使用令牌,向资源服务器申请获取资源。
  • (F)资源服务器确认令牌无误,同意向客户端开放资源

上述的六个步骤,B 是关键,即用户如何给客户端进行授权。有了授权之,客户端就可以获取令牌,进而凭令牌获取资源。

OAauth2.0 包括以下角色:
1、客户端
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android 客户端、Web 客户端(浏览器端)、微信客户端等。

2、资源拥有者
通常为用户,也可以是应用程序,即该资源的拥有者。

3、授权服务器(也称认证服务器)
用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌 (access_token) ,作为客户端访问资源服务器的凭据。本例为微信的认证服务器。

4、资源服务器
存储资源的服务器,本例子为微信存储的用户信息。
现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗? 答案是否定的,服务提供商会给准入的接入方一个身份,用于接入时的凭据:

client_id :客户端标识
client_secret :客户端秘钥

因此,准确来说,授权服务器对两种 OAuth2.0 中的两个角色进行认证授权,分别是资源拥有者客户端

6.2 Spring Cloud Security OAuth2

6.2.1 环境介绍

         Spring-Security-OAuth2 是对 OAuth2 的一种实现,并且跟之前学习的 Spring Security 相辅相成,与 Spring Cloud体系 的集成也非常便利,接下来,需要对它进行学习,最终使用它来实现案例设计的分布式认证授权解决方案。

         OAuth2.0 的服务提供方涵盖两个服务,即 授权服务 (Authorization Server,也叫认证服务)资源服务 (Resource Server) ,使用 Spring Security OAuth2 的时候可以选择把它们在同一个应用程序中实现,也可以选择建立使用同一个授权服务的多个资源服务。

授权服务 (Authorization Server) 应包含对接入端以及登入用户的合法性进行验证并颁发 token 等功能,对令牌的请求端点由 Spring MVC 控制器进行实现,下面是配置一个认证服务必须要实现的 endpoints

  • AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize
  • TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token

资源服务 (Resource Server) 应包含对资源的保护功能,对非法请求进行拦截,对请求中 token 进行解析鉴权等,下面的过滤器用于实现 OAuth 2.0 资源服务:

  • OAuth2AuthenticationProcessingFilter 用来对请求给出的身份令牌解析鉴权。

本教程分别创建 uaa授权服务(也可叫认证服务)order订单资源服务
在这里插入图片描述
认证流程如下:
1、客户端请求 UAA 授权服务进行认证。
2、认证通过后由 UAA 颁发令牌。
3、客户端携带令牌 token 请求资源服务
4、资源服务校验令牌的合法性,合法即返回资源信息。

6.2.2 环境搭建

6.2.2.1 父工程

创建 distributed‐security 项目为 maven 工程作为父工程,依赖如下:

<?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 http://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.6.1</version>-->
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.king.security</groupId>
    <artifactId>distributed-security</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- 指定父级工程 -->
    <packaging>pom</packaging>

    <modules>
        <!-- 引入子模块名称 -->
        <module>distributed-security-uaa</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>


            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>3.1.0</version>
                <scope>provided</scope>
            </dependency>

            <dependency>
                <groupId>javax.interceptor</groupId>
                <artifactId>javax.interceptor-api</artifactId>
                <version>1.2</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.47</version>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.0</version>
            </dependency>

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.11</version>
            </dependency>


            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.0.10.RELEASE</version>
            </dependency>


            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.1.3.RELEASE</version>
            </dependency>


        </dependencies>
    </dependencyManagement>


    <build>
        <finalName>${project.name}</finalName>
        <resources>
            <resource>
                <directory>untitled/src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
            <resource>
                <directory>untitled/src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <!--<plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>-->

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <encoding>utf-8</encoding>
                    <useDefaultDelimiters>true</useDefaultDelimiters>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
6.2.2.2 创建UAA授权服务工程

1、创建 distributed-security-uaa 子工程
创建 distributed-security-uaa 作为授权服务工程,依赖如下:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <!-- 引入父级模块名称 -->
    <parent>
        <artifactId>distributed-security</artifactId>
        <groupId>com.king.security</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <!-- 子模块名称 -->
    <artifactId>distributed-security-uaa</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</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-freemarker</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

</project>

工程结构如下:
在这里插入图片描述
2、启动类
本工程采用 SpringBoot 开发,每个工程编写一个启动类:

package com.king.security.distributed.uaa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(basePackages = {"com.king.security.distributed.uaa"})
public class UAAServer {

    public static void main(String[] args) {
        SpringApplication.run(UAAServer.class, args);
    }
}

3、配置文件
resources 下创建 application.properties 配置文件:

spring.application.name=uaa-service
server.port=53020
spring.main.allow-bean-definition-overriding = true

logging.level.root = debug
logging.level.org.springframework.web = info

spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true
server.tomcat.remote_ip_header = x-forwarded-for
server.tomcat.protocol_header = x-forwarded-proto
server.use-forward-headers = true
server.servlet.context-path = /uaa

spring.freemarker.enabled = true
spring.freemarker.suffix = .html
spring.freemarker.request-context-attribute = rc
spring.freemarker.content-type = text/html
spring.freemarker.charset = UTF-8
spring.mvc.throw-exception-if-no-handler-found = true
spring.resources.add-mappings = false

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user_db?characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=rootroot

eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env

feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true
6.2.2.3 创建Order资源服务工程

本工程为Order订单服务工程,访问本工程的资源需要认证通过。
本工程的目的主要是测试认证授权的功能,所以不涉及订单管理相关业务。

1、创建 Order 工程,pom.xml 文件:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <!-- 引入父级模块名称 -->
    <parent>
        <artifactId>distributed-security</artifactId>
        <groupId>com.king.security</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <!-- 子模块名称 -->
    <artifactId>distributed-security-order</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

</project>

2、工程结构:
在这里插入图片描述
3、配置文件
resources 中创建 application.properties 配置文件:

spring.application.name=order-service
server.port=53021
spring.main.allow-bean-definition-overriding = true

logging.level.root = debug
logging.level.org.springframework.web = info
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true
server.tomcat.remote_ip_header = x-forwarded-for
server.tomcat.protocol_header = x-forwarded-proto
server.use-forward-headers = true
server.servlet.context-path = /order


spring.freemarker.enabled = true
spring.freemarker.suffix = .html
spring.freemarker.request-context-attribute = rc
spring.freemarker.content-type = text/html
spring.freemarker.charset = UTF-8
spring.mvc.throw-exception-if-no-handler-found = true
spring.resources.add-mappings = false


eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env

feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true

6.2.2.授权服务器配置

6.2.2.1 EnableAuthorizationServer

可以用 @EnableAuthorizationServer 注解并继承 AuthorizationServerConfigurerAdapter 来配置 OAuth2.0 授权服务器。

distributed-security-uaa 工程的 Config 包下创建 AuthorizationServer 配置文件 :

/**
 * @EnableAuthorizationServer:认证授权服务,提供用于获取token,解析token相关功能,实现认证、授权功能。
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
		// 略...
}

AuthorizationServerConfigurerAdapter 要求配置以下几个类,这几个类是由 Spring 创建的独立的配置对象,它们会被 Spring 传入AuthorizationServerConfigurer 中进行配置:

package com.king.security.distributed.uaa.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

/**
 * 授权配置类
 * @EnableAuthorizationServer:认证授权服务,提供用于获取token,解析token相关功能,实现认证、授权功能。
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    public AuthorizationServer() {
        super();
    }

    /**
     * 用来配置令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        super.configure(security);
    }

    /**
     * 配置客户端详情服务,客户端鉴权配置 client_id :客户端标识、client_secret :客户端秘钥
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        super.configure(clients);
    }

    /**
     * 用来配置令牌(token)的访问端点和令牌服务(token services)
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
    }
}
  • ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
  • AuthorizationServerEndpointsConfigurer:用来配置令牌( token )的访问端点和令牌服务( token services )。
  • AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束。

大体流程:ClientDetailsServiceConfigurer 客户端来 AuthorizationServerEndpointsConfigurer 申请令牌,申请完令牌之后需要 AuthorizationServerSecurityConfigurer 管理约束令牌。

6.2.2.2.1 配置客户端详细信息

ClientDetailsServiceConfigurer能够使用内存或者 JDBC 来实现客户端详情服务(ClientDetailsService), ClientDetailsService 负责查找 ClientDetails ,而ClientDetails 有几个重要的属性如下列表:

  • clientId:(必须的)用来标识客户的 id
  • secret:(需要值得信任的客户端)客户端安全码,如果有的话。
  • scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过自己实现 ClientRegistrationService 接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。

暂时使用内存方式存储客户端详情信息,在distributed-security-uaa 工程的 Config 包下的 AuthorizationServer 配置文件,public void configure(ClientDetailsServiceConfigurer clients) throws Exception {} 方法配置如下:

   /**
     * 配置客户端详情服务,客户端鉴权配置 client_id :客户端标识、client_secret :客户端秘钥
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 暂时使用内存方式存储客户端详情信息
        clients.inMemory()// 使用in-memory存储
                .withClient("c1")// client_id 客户端ID
                .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥
                .resourceIds("res1")// 资源列表
                .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)// false会跳转到授权页面
                .redirectUris("http://www.baidu.com");// 加上验证回调地址
                // .and() // 配置多个客户端用 and 继续配置
                // .withClient("c2")// client_id 客户端ID
                // .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥
                // .resourceIds("res1")// 资源列表
                // .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                // .scopes("all")// 允许的授权范围
                // .autoApprove(false)// false会跳转到授权页面
                // .redirectUris("http://www.zhihu.com");// 加上验证回调地址
    }
6.2.2.2.2 配置自定义UserDetailsService

distributed-security-uaa 工程的 model 包下定义 UserDto 用户信息PermissionsDto 权限信息类:

package com.king.security.distributed.uaa.model;

import lombok.Data;

@Data
public class UserDto {

    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;

}
package com.king.security.distributed.uaa.model;

import lombok.Data;

@Data
public class PermissionsDto {

    private String id;
    private String code;
    private String description;
    private String url;
}

distributed-security-uaa 工程的 dao 包下定义 UserDao类:

package com.king.security.distributed.uaa.dao;

import com.king.security.distributed.uaa.model.PermissionsDto;
import com.king.security.distributed.uaa.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

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

@Repository
public class UserDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    /**
     * 根据用户名查询用户信息
     * @param username
     * @return
     */
    public UserDto getUserByUsername(String username){

        String sql ="select id,username,password,fullname from t_user where username = ?";

        List<UserDto> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));

        if(list == null || list.size() <= 0){
            return null;
        }

        return list.get(0);
    }

    /**
     * 根据用户id查询用户权限
     * @param userId
     * @return
     */
    public List<String> findPermissionsByUserId(String userId) {

        String sql = "SELECT * FROM t_permission WHERE id IN( " +
                "SELECT t_role_permission.permission_id FROM t_role_permission WHERE t_role_permission.role_id IN ( " +
                "SELECT t_user_role.role_id FROM t_user_role WHERE t_user_role.user_id = ? ) )";

        List<PermissionsDto> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionsDto.class));

        List<String> permissionsList = new ArrayList<>();

        list.forEach(permissionsDto -> permissionsList.add(permissionsDto.getCode()));

        return permissionsList;
    }

}

distributed-security-uaa 工程的 service 包下自定义配置 UserDetailsService类:

package com.king.security.distributed.uaa.service;

import com.king.security.distributed.uaa.dao.UserDao;
import com.king.security.distributed.uaa.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
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.List;

/**
 * 自定UserDetailsService用户认证器
 */
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    /**
     * 根据用户账号查询信息
     * @param username 用户账号
     * @return 返回用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //登录账号
        System.out.println("username="+username);

        //根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
        // UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
        // return userDetails;

        //根据账号去数据库查询...
        UserDto userDto = userDao.getUserByUsername(username);

        if(userDto == null){
            return null;
        }

        //查询用户权限
        List<String> permissionsList = userDao.findPermissionsByUserId(userDto.getId());

        // 定义一个数据
        String[] permissionsArray = new String[permissionsList.size()];

        // 将 permissionsList 转成 数组
        permissionsList.toArray(permissionsArray);
        
		// 将userDto转成json
        String principal = JSON.toJSONString(userDto);

        // 创建userDetails
        UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionsArray).build();
        return userDetails;
    }
}
6.2.2.2.3 管理令牌

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。

自己可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类, 里面包含了一些有用实现,可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都 实现了 TokenStore 接口:

  • InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
  • JdbcTokenStore:这是一个基于 JDBC 的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把 "spring-jdbc" 这个依赖加入到你的 classpath 当中。
  • JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。 另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。

1、定义 TokenConfig
distributed-security-uaa 工程的 config包下定义 TokenConfig ,暂时先使用 InMemoryTokenStore ( token 存储在内存) token 生成策略时,系统默认的token 的有效时间是 12 小时 ,生成一个普通的令牌:

package com.king.security.distributed.uaa.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * token令牌配置类
 */
@Configuration
public class TokenConfig {

    /**
     * 令牌储存策略
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        // 使用内存存储令牌(普通令牌)
        return new InMemoryTokenStore();
    }

}

2、定 AuthorizationServerTokenServices
distributed-security-uaa 工程的 Config 包下的 AuthorizationServer 配置文件中定义 AuthorizationServerTokenServices() 方法:

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 令牌管理服务
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);//客户端详情服务
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略
        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }
6.2.2.2.4 令牌访问端点配置

AuthorizationServerEndpointsConfigurer 这个对象的实例可以完成令牌服务以及令牌 endpoint 配置。

配置授权类型(Grant Types):
AuthorizationServerEndpointsConfigurer 通过设定以下属性决定支持的授权类型(Grant Types)

  • authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。
  • userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现, 或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对象),当你设置了这个之后,那么 "refresh_token" 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。
  • authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对 象),主要用于 "authorization_code" 授权码类型模式。
  • implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
  • tokenGranter:当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。

配置授权端点的URL(Endpoint URLs):
AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做 pathMapping() 的方法用来配置端点 URL链接,它有两个参数:

  • 第一个参数: String 类型的,这个端点 URL 的默认链接。
  • 第二个参数: String 类型的,你要进行替代的 URL 链接。

以上的参数都将以 "/" 字符为开始的字符串,框架的默认 URL 链接如下列表,可以作为这个 pathMapping() 方法的第一个参数:

  • /oauth/authorize :授权端点。
  • /oauth/token:令牌端点。
  • /oauth/confirm_access:用户确认授权提交端点。
  • /oauth/error :授权服务错误信息端点。
  • /oauth/check_token :用于资源服务访问的令牌解析端点。
  • /oauth/token_key:提供公有密匙的端点,如果你使用 JWT 令牌的话。

需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问。

distributed-security-uaa 工程的 Config 包下的 AuthorizationServer 配置文件中配置令牌访问端点:

	@Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

	/**
     * 设置授权码模式的授权码如何存取
     * @param dataSource
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        return new JdbcAuthorizationCodeServices(dataSource);
    }
    
   /**
     * 用来配置令牌(token)的访问端点和令牌服务(token services)
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)//认证管理器(密码模式需要)
                .authorizationCodeServices(authorizationCodeServices)//授权码服务需要
                .tokenServices(tokenService())//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);//允信post提交
    }
6.2.2.2.5 令牌端点的安全约束

AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束,在distributed-security-uaa 工程的 Config 包下的 AuthorizationServer 配置文件中配置如下:

   /**
     * 用来配置令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		security
				.tokenKeyAccess("permitAll()") 			(1) 
				.checkTokenAccess("permitAll()") 		(2)
				.allowFormAuthenticationForClients();	(3)
	}

(1) tokenkey 这个 endpoint 当使用 JwtToken 且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint 完全公开。
(2)checkToken 校验令牌这个 endpoint 端点完全公开。
(3) 允许表单认证。

授权服务配置总结
授权服务配置分成三大块,可以关联记忆。

  1. 既然要完成认证,它首先得知道客户端信息从哪儿读取,因此要进行客户端详情配置。
  2. 既然要颁发 token ,那必须得定义 token 的相关 endpoint 端点 ,以及 token 如何存取,以及客户端支持哪些类型的 token
  3. 既然暴露除了一些 endpoint 端点,那对这些 endpoint 端点可以定义一些安全上的约束等。
6.2.2.2.6 web安全配置

distributed-security-uaa 工程的 Config 包下定义 WebSecurityConfig 配置文件:

package com.king.security.distributed.uaa.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * spring security安全配置
 * @EnableGlobalMethodSecurity:Spring Security默认是禁用注解的,要想开启注解,
 * 需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,
 * 来判断用户对某个控制层的方法是否具有访问权限,
 * securedEnabled: 确定安全注解 [@Secured] 是否启用
 * prePostEnabled: 确定 前置注解[@PreAuthorize,@PostAuthorize,..] 是否启用
 * jsr250Enabled: 确定 JSR-250注解 [@RolesAllowed..]是否启用
 **/
@Configuration
@EnableGlobalMethodSecurity( securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 定义用户信息认证服务(认证管理器)
     * @return
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 检验密码编码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        // 无需编码器进行匹配密码
        // return NoOpPasswordEncoder.getInstance();

        // 使用BCrypt方式进行匹配密码
        return new BCryptPasswordEncoder();
    }

    /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() // csrf().disable():屏蔽CSRF控制,即spring security不再限制CSRF
                .authorizeRequests()  //开启登录配置
                .antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
                .antMatchers("/login*")
                .permitAll() //除了/login*",其它的请求可以访问
                .anyRequest().authenticated()
                .and()
                .formLogin(); //允许表单登录

    }

}
6.2.2.2.7 oauth_code 跟 oauth_client_details 数据库表
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
  `code` varchar(255) DEFAULT NULL,
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `oauth_code` VALUES ('Ykj769', '2020-12-13 16:18:25', 0xACED0005737200416F72672E737072696E676672616D65776F726B2E73656375726974792E6F61757468322E70726F76696465722E4F417574683241757468656E7469636174696F6EBD400B02166252130200024C000D73746F7265645265717565737474003C4C6F72672F737072696E676672616D65776F726B2F73656375726974792F6F61757468322F70726F76696465722F4F4175746832526571756573743B4C00127573657241757468656E7469636174696F6E7400324C6F72672F737072696E676672616D65776F726B2F73656375726974792F636F72652F41757468656E7469636174696F6E3B787200476F72672E737072696E676672616D65776F726B2E73656375726974792E61757468656E7469636174696F6E2E416273747261637441757468656E7469636174696F6E546F6B656ED3AA287E6E47640E0200035A000D61757468656E746963617465644C000B617574686F7269746965737400164C6A6176612F7574696C2F436F6C6C656374696F6E3B4C000764657461696C737400124C6A6176612F6C616E672F4F626A6563743B787000737200266A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C654C697374FC0F2531B5EC8E100200014C00046C6973747400104C6A6176612F7574696C2F4C6973743B7872002C6A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C65436F6C6C656374696F6E19420080CB5EF71E0200014C00016371007E00047870737200136A6176612E7574696C2E41727261794C6973747881D21D99C7619D03000149000473697A65787000000002770400000002737200426F72672E737072696E676672616D65776F726B2E73656375726974792E636F72652E617574686F726974792E53696D706C654772616E746564417574686F7269747900000000000001FE0200014C0004726F6C657400124C6A6176612F6C616E672F537472696E673B787074000270317371007E000D74000270337871007E000C707372003A6F72672E737072696E676672616D65776F726B2E73656375726974792E6F61757468322E70726F76696465722E4F41757468325265717565737400000000000000010200075A0008617070726F7665644C000B617574686F72697469657371007E00044C000A657874656E73696F6E7374000F4C6A6176612F7574696C2F4D61703B4C000B726564697265637455726971007E000E4C00077265667265736874003B4C6F72672F737072696E676672616D65776F726B2F73656375726974792F6F61757468322F70726F76696465722F546F6B656E526571756573743B4C000B7265736F7572636549647374000F4C6A6176612F7574696C2F5365743B4C000D726573706F6E7365547970657371007E0016787200386F72672E737072696E676672616D65776F726B2E73656375726974792E6F61757468322E70726F76696465722E426173655265717565737436287A3EA37169BD0200034C0008636C69656E74496471007E000E4C001172657175657374506172616D657465727371007E00144C000573636F706571007E001678707400026331737200256A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C654D6170F1A5A8FE74F507420200014C00016D71007E00147870737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F400000000000067708000000080000000474000D726573706F6E73655F74797065740004636F6465740009636C69656E745F696471007E001974000C72656469726563745F75726C74001568747470733A2F2F7777772E62616964752E636F6D74000573636F706574000A524F4C455F41444D494E78737200256A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C65536574801D92D18F9B80550200007871007E0009737200176A6176612E7574696C2E4C696E6B656448617368536574D86CD75A95DD2A1E020000787200116A6176612E7574696C2E48617368536574BA44859596B8B7340300007870770C000000103F4000000000000171007E002478017371007E0028770C000000103F40000000000000787371007E001C3F40000000000000770800000010000000007874001568747470733A2F2F7777772E62616964752E636F6D707371007E0028770C000000103F4000000000000174000472657331787371007E0028770C000000103F4000000000000171007E001F787372004F6F72672E737072696E676672616D65776F726B2E73656375726974792E61757468656E7469636174696F6E2E557365726E616D6550617373776F726441757468656E7469636174696F6E546F6B656E00000000000001FE0200024C000B63726564656E7469616C7371007E00054C00097072696E636970616C71007E00057871007E0003017371007E00077371007E000B0000000277040000000271007E000F71007E00117871007E0033737200486F72672E737072696E676672616D65776F726B2E73656375726974792E7765622E61757468656E7469636174696F6E2E57656241757468656E7469636174696F6E44657461696C7300000000000001FE0200024C000D72656D6F74654164647265737371007E000E4C000973657373696F6E496471007E000E787074000F303A303A303A303A303A303A303A31740020413742323638324643314142443130313035324444414341353444413343453170737200326F72672E737072696E676672616D65776F726B2E73656375726974792E636F72652E7573657264657461696C732E5573657200000000000001FE0200075A00116163636F756E744E6F6E457870697265645A00106163636F756E744E6F6E4C6F636B65645A001563726564656E7469616C734E6F6E457870697265645A0007656E61626C65644C000B617574686F72697469657371007E00164C000870617373776F726471007E000E4C0008757365726E616D6571007E000E7870010101017371007E0025737200116A6176612E7574696C2E54726565536574DD98509395ED875B0300007870737200466F72672E737072696E676672616D65776F726B2E73656375726974792E636F72652E7573657264657461696C732E5573657224417574686F72697479436F6D70617261746F7200000000000001FE020000787077040000000271007E000F71007E00117870740008746F6D7368696469);

DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(64) NOT NULL,
  `resource_ids` varchar(255) DEFAULT NULL,
  `client_secret` varchar(255) DEFAULT NULL,
  `scope` varchar(255) DEFAULT NULL,
  `authorized_grant_types` varchar(255) DEFAULT NULL,
  `web_server_redirect_uri` varchar(255) DEFAULT NULL,
  `authorities` varchar(255) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` longtext,
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `archived` tinyint(4) DEFAULT NULL,
  `trusted` tinyint(4) DEFAULT NULL,
  `autoapprove` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `oauth_client_details` VALUES ('c1', 'res1', '$2a$10$Dappn49nM8ZjkCO3ASwR/evnHQN3iPH4npP45js0d9Ug3Vm4ui.sq', 'ROLE_ADMIN,ROLE_USER,ROLE_API', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', null, '7200', '259200', null, '2020-12-13 15:53:10', '0', '0', 'false');
INSERT INTO `oauth_client_details` VALUES ('c2', 'res2', 'secret', 'ROLE_API', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', null, '7200', '259200', null, '2020-12-13 15:53:10', '0', '0', 'false');

6.2.3.授权码模式

6.2.3.1 授权码模式介绍

下图是授权码模式交互图:
在这里插入图片描述
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:

/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

参数列表如下:

  • client_id:客户端准入标识。
  • response_type:授权码模式固定为 code
  • scope:客户端权限。
  • redirect_uri:跳转 uri ,当授权码申请成功后会跳转到此地址,并在后边带上 code 参数(授权码)。

(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
(3)授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)
(4)客户端拿着授权码向授权服务器索要访问 access_token ,请求如下:

/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com

参数列表如下:

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写 authorization_code,表示授权码模式。
  • code:授权码就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
  • redirect_uri:申请授权码时的跳转 url ,一定和申请授权码时用的 redirect_uri 一致。

(5)授权服务器返回令牌(access_token):
这种模式是四种模式中最安全的一种模式。一般用于 clientWeb 服务器端应用或第三方的原生 App 调用资源服务的时候。因为在这种模式中 access_token 不会经过浏览器或移动端的 App ,而是直接从服务端去交换,这样就最大限度的减小了令牌泄漏的风险。

6.2.3.2 测试授权码模式

启动distributed-security-uaa 工程,在浏览器访问认证页面:

GET http://localhost:53020/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

访问时被重定向到登录页,因为没有登录认证:
在这里插入图片描述
进行认证登录即可,认证之后跳转到授权页面:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
确认授权后,浏览器会重定向到指定路径(oauth_client_details表 中的 web_server_redirect_uri)并附加验证码? code=igAgar(每次不一样),最后使用该验证码获取 token
在这里插入图片描述

获取 token 令牌 URL : http://localhost:53020/uaa/oauth/token

在这里插入图片描述

6.2.4.简化模式

6.2.4.1 简化模式介绍下

图是简化模式交互图:
在这里插入图片描述
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:

http://localhost:53020/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com

参数描述同**授权码模式** ,注意 response_type=token ,说明是简化模式。
(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
(3)授权服务器将授权码将令牌(access_token)以 Hash 的形式存放在重定向uri的fargment中发送给浏览器。
注: fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过 (#)作为 fragment 的开头, 其中 # 不属于 fragment 的值。如 https://domain/index#L18 这个 URIL18 就是 fragment 的值。只需要知道 js 通过响应浏览器地址栏变化的方式能获取到 fragment 就行了。

一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。

6.2.4.2 测试简化模式

启动distributed-security-uaa 工程,在浏览器访问认证页面:

http://localhost:53020/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com

访问时被重定向到登录页,因为没有登录认证:
在这里插入图片描述
进行认证登录即可,认证之后跳转到授权页面:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
确认授权后,浏览器会重定向到指定路径(oauth_client_details表 中的web_server_redirect_uri)并以 Hash 的形式存放在重定向 urifargment 中,如:

https://www.baidu.com/#access_token=ef2d6701-7a74-4565-b8f5-2889b5283ac9&token_type=bearer&expires_in=3952

在这里插入图片描述

6.2.5.密码模式

6.2.5.1 授权码模式介绍

下图是密码模式交互图:
在这里插入图片描述
(1)资源拥有者将用户名、密码发送给客户端
(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:

http://localhost:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123

参数列表如下:

  • client_id:客户端准入标识。
  • client_secre:客户端秘钥。
  • grant_type:授权类型,填写 password 表示密码模式。username:资源拥有者用户名password:资源拥有者密码

(3)授权服务器将令牌(access_token)发送给client:
这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了 client ,因此这就说明这种模式只能用于 client 是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生 App 或第一方单页面应用。

6.2.5.2 测试密码模式

POST http://localhost:53020/uaa/oauth/token

参数列表如下:
在这里插入图片描述

6.2.6.客户端模式

6.2.6.1 客户端模式介绍

在这里插入图片描述
(1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)。
(2)确认客户端身份无误后,将令牌(access_token)发送给 client ,请求如下:

http://localhost:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials

参数列表如下:

  • client_id:客户端准入标识。
  • client_secre:客户端秘钥。
  • grant_type:授权类型,填写 client_credentials 表示客户端模式。

这种模式是最方便但最不安全的模式。因此这就要求我们对 client 完全的信任,而 client 本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。

6.2.6.2 测试客户端模式

POST http://localhost:53020/uaa/oauth/toke

请求参数:
在这里插入图片描述

6.2.7.资源服务测试

6.2.7.1 资源服务器配置

@EnableResourceServer 注解到一个 @Configuration 配置类上,并且必须使用 ResourceServerConfigurer 这个配置对象来进行配置(可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,参数就是这个对象的实例),下面是一些可以配置的属性:

ResourceServerSecurityConfigurer 中主要包括:

  • tokenServices : ResourceServerTokenServices 类的实例,用来实现令牌服务。
  • tokenStore : TokenStore类的实例,指定令牌如何访问,与 tokenServices 配置可选 。
  • resourceId : 这个资源服务的 ID ,这个属性是可选的,但是推荐设置并在授权服务中进行验证。 其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。

HttpSecurity 配置这个与 Spring Security类似:

  • 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是保护资源服务的全部路径。
  • 通过 http.authorizeRequests() 来设置受保护资源的访问规则。
  • 其他的自定义权限保护规则通过 HttpSecurity 来进行配置。

@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链。

distributed-security-order 工程的 Config 包下定义编写 ResouceServerConfig 配置文件:

package com.king.security.distributed.order.config;

import org.springframework.beans.factory.annotation.Autowired;
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.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;

/**
 * 资源服务配置类
 * @EnableResourceServer:资源服务,提供基于token的资源访问功能。
 */
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    /**
     * 资源ID,这个要跟授权服务端的授权资源列表的值对应才能正确找到资源
     */
    public static final String RESOURCE_ID = "res1";

    @Autowired
    TokenStore tokenStore;

    /**
     * 资源服务端令牌解析服务
     * @return
     */
    @Bean
    public ResourceServerTokenServices tokenService() {
        //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }

    /**
     * 资源服务器安全配置器
     * @param resources
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)//资源 id
                // .tokenStore(tokenStore)
               .tokenServices(tokenService())//验证令牌的服务
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('all')") //授权范围
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 现在基于token形式,这里就不记录session了
    }

}
6.2.7.2 验证token

ResourceServerTokenServices 是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用 DefaultTokenServices ,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices,它知道如何对令牌进行解码。

令牌解析方法: 使用 DefaultTokenServices 在资源服务器本地配置令牌存储、解码、解析方式 使用 RemoteTokenServices 资源服务器通过 HTTP 请求来解码令牌,每次都请求授权服务器端点 /oauth/check_token

使用授权服务的 /oauth/check_token 端点你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问, 这在咱们授权服务配置中已经提到了,下面是一个例子,在这个例子中,我们在distributed-security-uaa 工程授权服务端的 Config 包下 AuthorizationServer 授权类中配置了 /oauth/check_token/oauth/token_key 这两个端点:

package com.king.security.distributed.uaa.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import javax.sql.DataSource;

/**
 * 授权配置类
 * @EnableAuthorizationServer:认证授权服务,提供用于获取token,解析token相关功能,实现认证、授权功能。
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    //略......

    /**
     * 用来配置令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")// http://localhost:53010/uaa/oauth/token 获取token令牌这个endpoint端点当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个permitAll()完全公开
                .checkTokenAccess("permitAll()")// http://localhost:53020/uaa/oauth/check_token 校验token令牌这个endpoint端点完全公开
                .allowFormAuthenticationForClients();// 允许表单认证
    }

}

distributed-security-order 工程的 Config 包下的 ResouceServerConfig 中配置 RemoteTokenServices 资源服务令牌解析服务:

   /**
     * 资源ID,这个要跟授权服务端的授权资源列表的值对应才能正确找到资源
     */
    public static final String RESOURCE_ID = "res1";

    @Autowired
    TokenStore tokenStore;

    /**
     * 资源服务端令牌解析服务
     * @return
     */
    @Bean
    public ResourceServerTokenServices tokenService() {
        //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }

    /**
     * 资源服务器安全配置器
     * @param resources
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)//资源 id
                // .tokenStore(tokenStore)
               .tokenServices(tokenService())//验证令牌的服务
                .stateless(true);//用于指示仅允许对这些资源进行基于令牌的身份验证的标志。
    }

测试 http://localhost:53020/uaa/oauth/check_token 验证 token 令牌 URL
在这里插入图片描述
在这里插入图片描述

6.2.7.3 编写控制层

distributed-security-order 工程的 controller 包下编写 OrderController,此 controller 表示订单资源的访问类:

package com.king.security.distributed.order.controller;

import com.king.security.distributed.order.model.UserDTO;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @GetMapping(value = "/r1")
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问此url
    public String r1(){
        
        return "访问资源1";
    }

}
6.2.7.4 添加安全访问控制
package com.king.security.distributed.order.config;

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.configuration.WebSecurityConfigurerAdapter;

/**
 * spring security安全配置
 * @EnableGlobalMethodSecurity:Spring Security默认是禁用注解的,要想开启注解,
 * 需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,
 * 来判断用户对某个控制层的方法是否具有访问权限,
 * securedEnabled: 确定安全注解 [@Secured] 是否启用
 * prePostEnabled: 确定 前置注解[@PreAuthorize,@PostAuthorize,..] 是否启用
 * jsr250Enabled: 确定 JSR-250注解 [@RolesAllowed..]是否启用
 **/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
//                .antMatchers("/r/r1").hasAuthority("p2")
//                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll();//除了/r/**,其它的请求可以访问
    }

}
6.2.7.5 测试

1、同时启动 distributed-security-uaa 授权服务端跟 distributed-security-order 资源服务端,在没有授权 token 令牌时或授权 token 令牌是错误的,直接访问 distributed-security-order 工程的资源服务端是会被拒绝:
在这里插入图片描述
在这里插入图片描述

2、申请令牌
这里我们使用密码方式:
在这里插入图片描述
3、请求资源
按照 Oauth2.0协议 要求,请求资源需要携带token,如下: token的参数名称为:Authorization,值为:Bearer token值
在这里插入图片描述

6.3 JWT令牌

6.3.1 JWT介绍

         通过上边的测试发现,当资源服务和授权服务不在一起时资源服务使用RemoteTokenServices 远程请求授权服务验证 token,如果访问量较大将会影响系统的性能

解决上边问题:
         令牌采用 JWT格式 即可解决上边的问题,用户认证通过会得到一个 JWT令牌JWT令牌中已经包括了用户相关的信息,客户端只需要携带 JWT 访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权

1、什么是JWT?
        JSON Web Token(JWT) 是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递 json对象 ,传递的信息经过数字签名可以被验证和信任。JWT 可以使用 HMAC算法 或使用 RSA的公钥/私钥对来签名,防止被篡改。

官网:https://jwt.io/
标准:https://tools.ietf.org/html/rfc7519

JWT令牌的优点:

  1. jwt 基于 json ,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展。
  3. 通过非对称加密算法及数字签名技术,JWT 防止篡改,安全性高。
  4. 资源服务端使用 JWT 可不依赖认证服务端即可完成授权。

缺点:

  1. JWT 令牌较长,占存储空间比较大。

2、JWT令牌结构
通过学习 JWT 令牌结构为自定义 jwt 令牌打好基础。
JWT 令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

  • Header : 头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256RSA)。
    一个例子如下:
    下边是 Header 部分的内容

{
    “alg”: “HS256”,
    “typ”: “JWT”
}

  • Payload : 第二部分是内容负载,内容也是一个 json 对象,它是存放有效信息的地方,它可以存放 jwt 提供的现成字段,比如 : iss(签发者),exp(过期时间戳),sub(面向的用户)等,也可自定义字段。
    此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第二部分负载使用 Base64Url 编码,得到一个字符串就是 JWT 令牌的第二部分。 一个例子:

{
    “sub”: “1234567890”,
     “name”: “456”,
     “admin”: true
}

  • Signature : 第三部分是签名,此部分用于防止 jwt 内容被篡改。
    这个部分使用 base64url 将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用 header 中声明签名算法进行签名。
    一个例子:

签名算法 HMACSHA256(
    base64UrlEncode(header) + “.” +
    base64UrlEncode(payload),
    secret)

base64UrlEncode(header) : jwt 令牌的第一部分。
base64UrlEncode(payload) : jwt 令牌的第二部分。
secret : 签名所使用的密钥。

6.3.2 配置JWT令牌服务

distributed-security-uaa 授权工程中配置 jwt 令牌服务,即可实现生成 jwt格式的令牌。
1、TokenConfig

package com.king.security.distributed.uaa.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * token令牌配置类
 */
@Configuration
public class TokenConfig {
    /**
     * 签名密钥
     */
    private String SIGNING_KEY = "uaa123";

    /**
     * 令牌储存策略
     * @return
     */
    // @Bean
    // public TokenStore tokenStore() {
    //     // 使用内存存储令牌(普通令牌)
    //     return new InMemoryTokenStore();
    // }
    @Bean
    public TokenStore tokenStore() {
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * JWT访问令牌转换器(JWT算法)
     * @return
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }

}

2、定义 JWT 令牌服务

distributed-security-uaa 授权工程中修改 AuthorizationServer 授权配置类的 tokenService() 方法:

	@Autowired
    private JwtAccessTokenConverter accessTokenConverter;

   /**
     * 令牌管理服务
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);//客户端详情服务
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        // 令牌增强,生成JWT令牌
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }

6.3.3 生成jwt令牌

同时启动 distributed-security-uaa 授权服务端跟 distributed-security-order 资源服务端,以 POST 方式访问 http://localhost:53020/uaa/oauth/token
在这里插入图片描述

6.3.4 校验jwt令牌

资源服务需要和授权服务拥有一致的签字、令牌服务等:
1、将授权服务端中的 TokenConfig类 拷贝到资源服务端中
2、屏蔽资源服务端原来的令牌服务类方法

package com.king.security.distributed.order.config;

import org.springframework.beans.factory.annotation.Autowired;
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.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;

/**
 * 资源服务配置类
 * @EnableResourceServer:资源服务,提供基于token的资源访问功能。
 */
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    /**
     * 资源ID,这个要跟授权服务端的授权资源列表的值对应才能正确找到资源
     */
    public static final String RESOURCE_ID = "res1";

	/**
     * 注入令牌储存策略
     */
    @Autowired
    TokenStore tokenStore;

    /**
     * 资源服务端令牌解析服务
     * @return
     */
    // @Bean
    // public ResourceServerTokenServices tokenService() {
    //     //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
    //     RemoteTokenServices service=new RemoteTokenServices();
    //     service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
    //     service.setClientId("c1");
    //     service.setClientSecret("secret");
    //     return service;
    // }

    /**
     * 资源服务器安全配置器
     * @param resources
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)//资源 id
                .tokenStore(tokenStore)
               // .tokenServices(tokenService())//远程验证令牌的服务
                .stateless(true);//用于指示仅允许对这些资源进行基于令牌的身份验证的标志。
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('all')") //授权范围
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 现在基于token形式,这里就不记录session了
    }

}

3、测试
1)申请 jwt 令牌
在这里插入图片描述

2)使用令牌请求资源
在这里插入图片描述
小技巧:
令牌申请成功可以使用 http://localhost:53020/uaa/oauth/check_token 校验令牌的有效性,并查询令牌的内容,例子如下:
在这里插入图片描述

6.4 完善数据库表环境配置

截止目前客户端信息和授权码仍然存储在内存中,生产环境中通过会存储在数据库中,下边完善环境的配置:

6.4.1 创建表

user_db数据库 中创建如下表:

oauth_client_details表,Spring Security OAuth2使用,用来存客户端详情信息:

DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '客户端id',
  `resource_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '允许访问资源列表',
  `client_secret` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '客户端密钥',
  `scope` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '授权范围',
  `authorized_grant_types` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'client允许的授权类型',
  `web_server_redirect_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '回调地址',
  `authorities` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔',
  `access_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的access_token的有效时间值(单位:秒),可选',
  `refresh_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的refresh_token的有效时间值(单位:秒),可选',
  `additional_information` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '这是一个预留的字段,在Oauth的流程中没有实际的使用,可选',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据的创建时间,精确到秒',
  `archived` tinyint(4) DEFAULT NULL COMMENT '用于标识客户端是否已存档(即实现逻辑删除),默认值为''0''(即未存档).',
  `trusted` tinyint(4) DEFAULT NULL COMMENT '设置客户端是否为受信任的,默认为''0''(即不受信任的,1为受信任的).',
  `autoapprove` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '设置用户是否自动Approval操作, 默认值为 ''false''',
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='客户端详情服务信息 ';

INSERT INTO `oauth_client_details` VALUES ('c1', 'res1', '$2a$10$Dappn49nM8ZjkCO3ASwR/evnHQN3iPH4npP45js0d9Ug3Vm4ui.sq', 'ROLE_ADMIN,ROLE_USER,ROLE_API', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', null, '7200', '259200', null, '2020-12-13 15:53:10', '0', '0', 'false');
INSERT INTO `oauth_client_details` VALUES ('c2', 'res2', 'secret', 'ROLE_API', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', null, '7200', '259200', null, '2020-12-13 15:53:10', '0', '0', 'false');

oauth_code表,Spring Security OAuth2使用,用来存储授权码:

DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
  `code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '存储服务端系统生成的code的值(未加密).',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)',
  `authentication` blob COMMENT '存储将AuthorizationRequestHolder.java对象序列化后的二进制数据.'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='存储授权码';

spring-oauth-server 数据库表说明

6.4.2 配置授权服务

(1)distributed-security-uaa 工程授权服务端的 Config 包下 AuthorizationServer 授权类中配置修改 ClientDetailsServiceAuthorizationCodeServices 从数据库读取数据 :

package com.king.security.distributed.uaa.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.sql.DataSource;
import java.util.Arrays;

/**
 * 授权配置类
 * @EnableAuthorizationServer:认证授权服务,提供用于获取token,解析token相关功能,实现认证、授权功能。
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;
    /**
     * 注入检验密码编码器
     */
    @Autowired
    PasswordEncoder passwordEncoder;

    /**
     * 将客户端详情服务信息存储到数据库
     * @param dataSource
     * @return
     */
    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }

    /**
     * 配置客户端详情服务,客户端鉴权配置 client_id :客户端标识、client_secret :客户端秘钥
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 暂时使用内存方式存储客户端详情信息
        // clients.inMemory()// 使用in-memory存储
        //         .withClient("c1")// client_id 客户端ID
        //         .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥
        //         .resourceIds("res1")// 资源列表
        //         .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
        //         .scopes("all")// 允许的授权范围
        //         .autoApprove(false)// false会跳转到授权页面
        //         .redirectUris("http://www.baidu.com");// 加上验证回调地址
                // .and() // 配置多个客户端用 and 继续配置
                // .withClient("c2")// client_id 客户端ID
                // .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥
                // .resourceIds("res1")// 资源列表
                // .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                // .scopes("all")// 允许的授权范围
                // .autoApprove(false)// false会跳转到授权页面
                // .redirectUris("http://www.zhihu.com");// 加上验证回调地址

        // 使用数据库方式存储客户端详情信息
        clients.withClientDetails(clientDetailsService);
    }

    /**
     * 令牌管理服务
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);//客户端详情服务
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        // 令牌增强,生成JWT令牌
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }

    /**
     * 设置授权码模式的授权码
     * @param dataSource
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        // 设置授权码模式的授权码如何存取,暂时采用内存方式
        // return new InMemoryAuthorizationCodeServices();

        // 采用数据库方式
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    /**
     * 用来配置令牌(token)的访问端点和令牌服务(token services)
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)//认证管理器(密码模式需要)
                .authorizationCodeServices(authorizationCodeServices)//授权码服务需要
                .tokenServices(tokenService())//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);//允信post提交
    }

    /**
     * 用来配置令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")// http://localhost:53010/uaa/oauth/token 获取token令牌这个endpoint端点当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个permitAll()完全公开
                .checkTokenAccess("permitAll()")// http://localhost:53020/uaa/oauth/check_token 校验token令牌这个endpoint端点完全公开
                .allowFormAuthenticationForClients();// 允许表单认证
    }

}

6.4.3测试

1、测试申请令牌
使用密码模式申请令牌,客户端信息需要和数据库中的信息一致。
在这里插入图片描述

2、测试授权码模式
生成的授权存储到数据库中。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

7.Spring Security实现分布式系统授权

7.1 需求分析

回顾技术方案如下:
在这里插入图片描述
1、 distributed-security-uaa 认证服务负责认证授权。
2、所有请求经过网关到达微服务。
3、网关负责鉴权客户端以及请求转发。
4、网关将 token 解析后传给微服务,微服务进行授权。

7.2.注册中心

        所有微服务的请求都经过网关,网关从注册中心读取微服务的地址,将请求转发至微服务。

本节完成注册中心的搭建,注册中心采用 Eureka
1、创建 distributed-security-discovery 注册中心服务为 maven 工程,工程结构:
在这里插入图片描述
2、pom.xml 依赖如下:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- 引入父级模块名称 -->
    <parent>
        <artifactId>distributed-security</artifactId>
        <groupId>com.king.security</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <!-- 子模块名称 -->
    <artifactId>distributed-security-discovery</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

    </dependencies>


</project>

3、配置文件
resources 中配置 application.yml

spring:
  application:
    name: distributed-discovery

server:
  port: 53000 # 启动端口

# eureka 访问地址:http://localhost:53000
eureka:
  server:
    enable-self-preservation: false    # 关闭服务器自我保护,客户端心跳检测15分钟内错误达到80%服务会保护,导致别人还认为是好用的服务
    eviction-interval-timer-in-ms: 10000 # 清理间隔(单位毫秒,默认是60*1000)5秒将客户端剔除的服务在服务注册列表中剔除#
    shouldUseReadOnlyResponseCache: true # eureka是CAP理论种基于AP策略,为了保证强一致性关闭此切换CP 默认不关闭 false关闭
  client:
    register-with-eureka: false  # false:不作为一个客户端注册到注册中心
    fetch-registry: false      # 为true时,可以启动,但报异常:Cannot execute request on any known server
    instance-info-replication-interval-seconds: 10
    serviceUrl:
      defaultZone: http://localhost:${server.port}/eureka/
  instance:
    hostname: ${spring.cloud.client.ip-address}
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}

启动类:

package com.king.security.distributed.discovery;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class DiscoveryServer {

    public static void main(String[] args) {
        SpringApplication.run(DiscoveryServer.class,args);
    }
}

7.3.网关 zuul

        网关整合 OAuth2.0 有两种思路,一种是认证服务器生成 jwt令牌, 所有请求统一在网关层验证,判断权限等操作;另一种是由各资源服务处理,网关只做请求转发。

        这里选用第一种。把 API网关 作为 OAuth2.0 的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息(jsonToken)给微服务,这样下游微服务就不需要关心令牌格式解析以及 OAuth2.0 相关机制了。

API网关 在认证授权体系里主要负责两件事:
(1)作为OAuth2.0的资源服务器角色,实现接入方权限拦截。
(2)令牌解析并转发当前登录用户信息(明文token)给微服务

微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:
(1)用户授权拦截(看当前用户是否有权访问该资源)
(2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)

7.3.1 创建工程

1、创建 distributed-security-gateway 网关服务为 maven 工程,工程结构:
在这里插入图片描述

2、pom.xml

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <!-- 引入父级模块名称 -->
    <parent>
        <artifactId>distributed-security</artifactId>
        <groupId>com.king.security</groupId>
        <version>1.0-SNAPSHOT</version>
     </parent>

    <modelVersion>4.0.0</modelVersion>

    <!-- 子模块名称 -->
    <artifactId>distributed-security-gateway</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>


</project>

3、配置文件
配置 application.properties 文件:

spring.application.name=gateway-server
server.port=53010
spring.main.allow-bean-definition-overriding = true

logging.level.root = info
logging.level.org.springframework = info

zuul.retryable = true
zuul.ignoredServices = *
zuul.add-host-header = true
zuul.sensitiveHeaders = *

zuul.routes.uaa-service.stripPrefix = false
zuul.routes.uaa-service.path = /uaa/**

zuul.routes.order-service.stripPrefix = false
zuul.routes.order-service.path = /order/**

eureka.client.serviceUrl.defaultZone= http://localhost:53000/eureka/
eureka.client.register-with-eureka= true
eureka.client.fetch-registry= true
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip?address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env

feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true

统一认证服务(distributed-security-uaa 授权服务端)与统一用户服务都是网关下微服务,需要在网关上新增路由配置:

zuul.routes.uaa‐service.stripPrefix = false
zuul.routes.uaa‐service.path = /uaa/**

zuul.routes.user‐service.stripPrefix = false
zuul.routes.user‐service.path = /order/**

上面配置了网关接收的请求 url 若符合 /order/** 表达式,将被被转发至distributed-security-order 资源服务端(统一用户服务)

启动类:

package com.king.security.distributed.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayServer {

    public static void main(String[] args) {
        SpringApplication.run(GatewayServer.class, args);
    }
}

7.3.2 token配置

         前面也介绍了,资源服务器由于需要验证并解析令牌,往往可以通过在授权服务器暴露 check_tokenEndpoint 来完成,而我们在授权服务器使用的是对称加密的 jwt,因此知道密钥即可,资源服务与授权服务本就是对称设计, 那把 distributed-security-uaa 授权服务端的 TokenConfig 令牌配置类拷贝到 distributed-security-gatewayconfig 包下就行 。

package com.king.security.distributed.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * token令牌配置类
 */
@Configuration
public class TokenConfig {
    /**
     * 签名密钥
     */
    private String SIGNING_KEY = "uaa123";

    /**
     * 令牌储存策略
     * @return
     */
    // @Bean
    // public TokenStore tokenStore() {
    //     // 使用内存存储令牌(普通令牌)
    //     return new InMemoryTokenStore();
    // }
    @Bean
    public TokenStore tokenStore() {
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * JWT访问令牌转换器(JWT算法)
     * @return
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }

}

7.3.3 配置资源服务

distributed-security-gatewayconfig 包下定义 ResouceServerConfig 资源服务配类,主要配置的内容就是定义一些匹配规则,描述某个接入客户端需要什么样的权限才能访问某个微服务,如:

package com.king.security.distributed.gateway.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
public class ResouceServerConfig {

    /**
     * 资源ID
     */
    public static final String RESOURCE_ID = "res1";

    /**
     * uaa资源服务配置
     */
    @Configuration
    @EnableResourceServer
    public class UAAServerConfig extends ResourceServerConfigurerAdapter {
        /**
         * 注入令牌储存策略
         */
        @Autowired
        private TokenStore tokenStore;

        /**
         * 资源服务器安全配置器
         * @param resources
         */
        @Override
        public void configure(ResourceServerSecurityConfigurer resources){
            resources.tokenStore(tokenStore) // 令牌储存策略
                    .resourceId(RESOURCE_ID) // 资源 id
                    .stateless(true); // 用于指示仅允许对这些资源进行基于令牌的身份验证的标志。
        }

        /**
         * 配置自定义认证和授权
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/uaa/**").permitAll(); // distributed-security-uaa授权工程下的 /uaa/** 所有请求可以访问
        }

    }

    /**
     * order资源,uaa资源服务配置
     */
    @Configuration
    @EnableResourceServer
    public class OrderServerConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;

        /**
         * 资源服务器安全配置器
         * @param resources
         */
        @Override
        public void configure(ResourceServerSecurityConfigurer resources){
            resources.tokenStore(tokenStore) // 令牌储存策略
                    .resourceId(RESOURCE_ID) // 资源 id
                    .stateless(true); // 用于指示仅允许对这些资源进行基于令牌的身份验证的标志。
        }

        /**
         * 自定义认证和授权
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')"); // distributed-security-order资源工程下的 /order/** 所有请求,scope要有ROLE_API权限可访问
        }
    }

    //配置其它的资源服务..
}

上面定义了两个微服务的资源,其中:
UAAServerConfig 指定了若请求匹配 /uaa/** 网关不进行拦截。

OrderServerConfig 指定了若请求匹配 /order/**,也就是访问统一用户服务,接入客户端需要有scope 中包含 read ,并且 authorities(权限) 中需要包含 ROLE_USER

由于 res1 这个接入客户端,read 包括 ROLE_ADMIN,ROLE_USER,ROLE_API 三个权限。

7.3.4 安全配置

distributed-security-gatewayconfig 包下定义 WebSecurityConfig spring security安全配置:

package com.king.security.distributed.gateway.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * spring security安全配置
 **/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 安全拦截机制(授权)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() // csrf().disable():屏蔽CSRF控制,即spring security不再限制CSRF
                .authorizeRequests()  //开启登录配置
                .antMatchers("/**").permitAll(); // 所有请求可以访问

    }

}

7.3.转发明文token给微服务

        通过 Zuul 过滤器的方式实现,目的是让下游微服务能够很方便的获取到当前的登录用户信息(明文token)。

(1)实现 Zuul 前置过滤器,完成当前登录用户信息提取,并放入转发微服务的 request 中:
distributed-security-gatewayfilter 包下定义 AuthFilter 配置:

package com.king.security.distributed.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.king.security.distributed.gateway.common.EncryptUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 自定义token传递拦截
 */
public class AuthFilter extends ZuulFilter {
    /**
     * 是否执行该过滤器,true代表需要过滤
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤类型
     * @return
     */
    @Override
    public String filterType() {
        // 前置过滤器
        return "pre";
    }

    /**
     * 过滤顺序
     * @return
     */
    @Override
    public int filterOrder() {
        // 优先级,数字越小越优先
        return 0;
    }

    /**
     * 过滤器的具体逻辑
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        /**
         * 1.获取令牌内容
         */
        // 上下文对象
        RequestContext ctx = RequestContext.getCurrentContext();
        // 拿到认证用户身份对象
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // 判断认证用户身份对象如果不是OAuth2数据格式就不进行解析,直接过滤掉
        if(!(authentication instanceof OAuth2Authentication)){

            return null;
        }

        // 将证用户身份对象数据转成OAuth2数据格式
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;

        // 获取用户认证信息
        Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
        // 取出用户身份信息
        String principal = userAuthentication.getName();

        /**
         * 2.组装明文token,转发给微服务,放入header,名称为json‐token
         */
        // 存储用户权限集合
        List<String> authoritiesList = new ArrayList<>();
        // 从userAuthentication取出权限,放在authorities
        userAuthentication.getAuthorities().stream().forEach(c -> authoritiesList.add(((GrantedAuthority) c).getAuthority()));

        // 拿到OAuth2Request请求对象
        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        // 从OAuth2Request请求对象获取其他请求数据
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        // 将数据类型转成object类型
        Map<String,Object> jsonToken = new HashMap<>(requestParameters);

        // 判断用户认证信息不为空是,将用户名、用户权限列表放到 jsonToken 中
        if(userAuthentication!=null){
            jsonToken.put("principal",principal);
            jsonToken.put("authorities",authoritiesList);
        }

        // 把身份信息和权限信息放在json中之后,再通过 Base64 加密后加入到上下文请求对象的http的header头部中,转发给微服务
        ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));

        return null;
    }
}

(2)将 filter 纳入 spring 容器 :
distributed-security-gatewayconfig 包下定义 ZuulConfig 配置:

package com.king.security.distributed.gateway.config;

import com.king.security.distributed.gateway.filter.AuthFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CorsFilter;

/**
 * 配置Zuul网关
 */
@Configuration
public class ZuulConfig {
    /**
     * 将自定义的前置过滤器加入到Spring容器中
     * @return
     */
    @Bean
    public AuthFilter preFileter() {
        return new AuthFilter();
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(18000L);

        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);

        FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

7.4.微服务用户鉴权拦截

        当微服务收到 明文token 时,应该怎么鉴权拦截呢?自己实现一个 filter ? 自己解析明文token ,自己定义一套资源访问策略?

        能不能适配 Spring Security 呢,是不是突然想起了前面我们实现的 Spring Security 基于 token 认证例子。还拿统一用户服务 distributed-security-order 作为网关下游微服务,对它进行改造,增加微服务用户鉴权拦截功能。

(1)增加测试资源,通过验证后并获取用户信息
distributed-security-orderOrderController 增加以下endpoint

package com.king.security.distributed.order.controller;

import com.king.security.distributed.order.model.UserDTO;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @GetMapping(value = "/r1")
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问此url
    public String r1() {
        // 因为通过 TokenAuthenticationFilter 类的 doFilterInternal() 方法的逻辑处理,将用户信息添加到 SecurityContextHolder 上下文对象中,这里就可以直接通过该对象获得用户信息
        UserDTO user = (UserDTO)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        
        return user.getUsername() + "访问资源1";
    }
    
    @GetMapping(value = "/r2")
    @PreAuthorize("hasAuthority('p2')")
    public String r2() {
        
        UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        
        return user.getUsername() + "访问资源2";
    }

}

(2)Spring Security配置
开启方法保护,并增加Spring配置策略,除了/login方法不受保护(统一认证要调用),其他资源全部需要认证才能访问。

distributed-security-orderconfig 包下 ResouceServerConfig 配置修改:

	@Override
    public void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                // .antMatchers("/**").access("#oauth2.hasScope('all')") // oauth2授权范围
                .antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')") // oauth2授权范围
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 现在基于token形式,这里就不记录session了
    }

综合上面的配置,共定义了三个资源了,拥有 p1 权限可以访问 r1 资源,拥有 p2 权限可以访问 r2 资源,只要认证通过就能访问 r3 资源。

(3)定义 filter 拦截 token ,并形成 Spring SecurityAuthentication 对象:

distributed-security-orderconfig 包下定义 TokenAuthenticationFilter 配置类:

package com.king.security.distributed.order.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.king.security.distributed.order.common.EncryptUtil;
import com.king.security.distributed.order.model.UserDTO;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 自定义每个请求过滤器一次
 */
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    /**
     * 它能够确保在一次请求中只通过一次filter,而不会重复执行
     * @param httpServletRequest
     * @param httpServletResponse
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        // 解析从 Zuul 网关转发过来明文token(header头部中的token)
        String token = httpServletRequest.getHeader("json-token");

        if(token != null){
            // 1.解析token
            // 因为 json-token 令牌在 Zuul网关加入到上下文请求对象的http的header头部中时通过Base64加了密,这要解密
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            // 将token转成json对象
            JSONObject jsonObject = JSON.parseObject(json);

            // 取出用户身份信息,将 principal的 json转成用户对象,方便Controller层获取用户信息
//            UserDTO userDTO = new UserDTO();
//            String principal = jsonObject.getString("principal");
//            userDTO.setUsername(principal);
            UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);

            // 用户权限
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");

            // 将用户权限列表转成数组对象
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);

            // 2.新建并填充authentication
            // 将用户信息和权限添加到用户名密码认证令牌对象中进行校验
            // 第一个参数:用户信息;第二参数:权重;第三个参数:用户权限列表
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));

            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));

            // 3.将authentication添加Spring Security安全上下文,Spring Security就会检验授权了
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        }

        // 执行过滤
        filterChain.doFilter(httpServletRequest,httpServletResponse);

    }
}

经过上边的过虑器,资源服务中就可以方便到的获取用户的身份信息:

UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

还是三个步骤:

  1. 解析 token
  2. 新建并填充 authentication
  3. authentication 保存进安全上下文剩下的事就交给 Spring Security 好了。

7.5.集成测试

本案例测试过程描述:
1、采用 OAuth2.0 的密码模式从 UAA 获取 token
2、使用该 token 通过网关访问订单服务的测试资源。

(1)通过网关访问 distributed-security-uaa 的授权及获取令牌,获取 token 。注意端口是53010,网关的端口。
如授权 endpoint :

GET http://localhost:53010/uaa/oauth/authorize?response_type=code&client_id=c1

在这里插入图片描述
在这里插入图片描述

令牌 endpoint ,最后通过授权码模式 code 换取 token 令牌:

POST http://localhost:53010/uaa/oauth/token

在这里插入图片描述

(2)使用 token 过网关访问订单服务中的r1-r2 测试资源进行测试。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(3)破坏 token 测试
token 测试返回内容:

{
       “error”: “unauthorized”,
       “error_description”: “Full authentication is required to access this resource”
}

破坏 token 测试返回内容:

{
       “error”: “invalid_token”,
       “error_description”: “Cannot convert access token to JSON”
}

7.6 扩展用户信息

7.6.1 需求分析

       目前 jwt 令牌存储了用户的身份信息、权限信息,网关将token明文化转发给微服务使用,目前用户身份信息仅包括了用户的账号,微服务还需要用户的 ID 、手机号等重要信息。所以,本案例将提供扩展用户信息的思路和方法,满足微服务使用用户信息的需求。

下边分析 JWT 令牌中扩展用户信息的方案:
       在认证阶段 DaoAuthenticationProvider 会调用 UserDetailService 查询用户的信息,这里是可以获取到齐全的用户信息的。由于JWT 令牌中用户身份信息来源于UserDetailsUserDetails 中仅定义了 username 为用户的身份信息, 这里有两个思路:第一是可以扩展 UserDetails ,使之包括更多的自定义属性,第二也可以扩展 username 的内容 ,比如存入 json 数据内容作为 username 的内容。相比较而言,方案二比较简单还不用破坏UserDetails 的结构,这里采用方案二。

7.6.2 修改UserDetailService

从数据库查询到 user,将整体 user 转成 json 存入 userDetails 对象。
distributed-security-uaaservice 包下修改 SpringDataUserDetailsService 配置类:

package com.king.security.distributed.uaa.service;

import com.alibaba.fastjson.JSON;
import com.king.security.distributed.uaa.dao.UserDao;
import com.king.security.distributed.uaa.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
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.List;

/**
 * 自定UserDetailsService用户认证器
 */
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    /**
     * 根据用户账号查询信息
     * @param username 用户账号
     * @return 返回用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //登录账号
        System.out.println("username="+username);

        //根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
        // UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
        // return userDetails;

        //根据账号去数据库查询...
        UserDto userDto = userDao.getUserByUsername(username);

        if(userDto == null){
            return null;
        }

        //查询用户权限
        List<String> permissionsList = userDao.findPermissionsByUserId(userDto.getId());

        // 定义一个数据
        String[] permissionsArray = new String[permissionsList.size()];

        // 将 permissionsList 转成 数组
        permissionsList.toArray(permissionsArray);

        //将userDto转成json
        String principal = JSON.toJSONString(userDto);

        // 创建userDetails
        UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionsArray).build();
        return userDetails;
    }
}

7.6.3 修改资源服务过虑器

资源服务中的过虑 器负责 从header中解析json-token,从中即可拿网关放入的用户身份信息,部分关键代码如下:

distributed-security-orderfilter 包下修改 TokenAuthenticationFilter 配置类:

package com.king.security.distributed.order.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.king.security.distributed.order.common.EncryptUtil;
import com.king.security.distributed.order.model.UserDTO;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 自定义每个请求过滤器一次
 */
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    /**
     * 它能够确保在一次请求中只通过一次filter,而不会重复执行
     * @param httpServletRequest
     * @param httpServletResponse
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        // 解析从 Zuul 网关转发过来明文token(header头部中的token)
        String token = httpServletRequest.getHeader("json-token");

        if(token != null){
            // 1.解析token
            // 因为 json-token 令牌在 Zuul网关加入到上下文请求对象的http的header头部中时通过Base64加了密,这要解密
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            // 将token转成json对象
            JSONObject jsonObject = JSON.parseObject(json);

            // 取出用户身份信息,将 principal的 json转成用户对象,方便Controller层获取用户信息
           // UserDTO userDTO = new UserDTO();
           // String principal = jsonObject.getString("principal");
           // userDTO.setUsername(principal);
            UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);

            // 用户权限
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");

            // 将用户权限列表转成数组对象
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);

            // 2.新建并填充authentication
            // 将用户信息和权限添加到用户名密码认证令牌对象中进行校验
            // 第一个参数:用户信息;第二参数:权重;第三个参数:用户权限列表
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));

            System.out.println("4------->" + authenticationToken.getPrincipal());

            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));

            // 3.将authentication添加Spring Security安全上下文,Spring Security就会检验授权了
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        // 执行过滤
        filterChain.doFilter(httpServletRequest,httpServletResponse);

    }

}

附录

HttpSecurity

HttpSecurity 配置列表:

方法说明
openidLogin()用于基于 OpenId 的验证
headers()用于基于 OpenId 的验证
cors()配置跨域资源共享( CORS )
sessionManagement()允许配置会话管理
portMapper()允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定 向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口 8443,HTTP 端口80到 HTTPS 端口443
jee()配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509()配置基于x509的认证
rememberMe允许配置“记住我”的验证
authorizeRequests()允许基于使用HttpServletRequest限制访问
requestCache()允许配置请求缓存
exceptionHandling()允许配置错误处理
securityContext()在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将 自动应用
servletApi()将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf()添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout()添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来 清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous()允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用 org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin()指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login()根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel()配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
addFilterAt()在指定的Filter类的位置添加过滤器
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值