Spring Security OAuth 2.0 认证授权。

Spring Security OAuth 2.0 认证授权。


文章目录


认证。

输入账号密码登录微信的过程就是认证。
用户名密码登录、指纹打卡、刷脸、二维码、手机短信。
认证是为了保护系统的隐私数据与资源。



会话。

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

基于 session 的认证方式。

用户认证成功后,在服务端生成用户相关的数据存在 session (当前会话)中,发给客户端的 session_id 存放到 cookie 中。这样用户客户端请求时带上 session_id 就可以验证服务器是否存在 session 数据,以此完成用户的合法校验。当用户退出系统或 session 过期销毁时,客户端的 session_id 也就无效了。
~
基于 session 的认证方式由 Servlet 规范制定,服务端要存储 session 的信息需要占用内存资源,客户端需要支持 cookie。

在这里插入图片描述



基于 token 的认证方式。

用户认证成功后,服务端生成一个 token 发送给客户端,客户端可以放到 cookie 或 localStorage 等存储中,每次请求时带上 token,服务端收到 token 通过验证后即可确认用户身份。
~
基于 token 的方式一般则不需要服务端存储 token,并且不限制客户端的存储方式。

在这里插入图片描述



授权。

认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分。授权是在认证通过之后发生的,控制不同的用户能够访问不同的资源。
~
用户登录后即可使用微信功能,eg. 发红包,朋友圈、添加好友等。而只有绑定过银行卡的用户才可以发红包。

授权的数据模型。

who 对 what 做了 how 操作。

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

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



RBAC。

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

Role-Based Access Control。

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

如果需求改为:总经理和部门经理。——> 改代码

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

可扩展性差。



Resource-Based Access Control。

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



基于 session 的认证方式。

方法含义
public HttpSession getSession(boolean create);获取当前 HttpSession 对象。
public void setAttribute(String name, Object o);向 session 中存放对象。
public Object getAttribute(String name);从 session 中获取对象。
public void removeAttribute(String name);移除 session 中的对象。
public void invalidate();使 HttpSession 失效。
  • Maven 工程。

SpringMVC、Servlet 3.0 环境搭建。

在这里插入图片描述

  • 配置类。
package com.geek.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;

@Configuration// applicationContext.xml。
@ComponentScan(basePackages = "com.geek.security.springmvc",
        excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)}
        // 排除 web 层 Controller。
)
public class ApplicationConfig {
    // 在此配置除了 Controller 的其他 Bean。eg. 数据库连接池、事务管理器、业务 Bean 等。
}

package com.geek.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.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration// SpringMVC.xml。
@EnableWebMvc
@ComponentScan(basePackages = "com.geek.security.springmvc",
        includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)}
)
public class WebConfig implements WebMvcConfigurer {

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

}

  • interface WebApplicationInitializer

只要最终实现了此接口,就能在容器启动时执行类中的方法。

com.geek.security.springmvc.init.SpringApplicationInitializer

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

public abstract class AbstractAnnotationConfigDispatcherServletInitializer extends AbstractDispatcherServletInitializer {

public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {

public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.web;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;

public interface WebApplicationInitializer {
    void onStartup(ServletContext var1) throws ServletException;
}

package com.geek.security.springmvc.init;

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

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /*
    public interface WebApplicationInitializer {
        void onStartup(ServletContext var1) throws ServletException;
    }
    只要最终实现了此接口,就能在容器启动时执行类中的方法。
    */

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

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

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



实现认证功能。
<%--
  Created by IntelliJ IDEA.
  User: geek
  Date: 2020/4/10
  Time: 20:09
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>用户登录。</title>
</head>
<body>

<form action="login" method="post">
    用户名:<input type="text" name="username"><hr>

<br>&nbsp;&nbsp;&nbsp;码:<input type="password" name="password"> <hr>

<br>
    <input type="submit" value="登录">
</form>

</body>
</html>

package com.geek.security.springmvc.config;

import com.geek.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.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration// SpringMVC.xml。
@EnableWebMvc
@ComponentScan(basePackages = "com.geek.security.springmvc",
        includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)}
)
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

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

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
    }
}

  • model 包。
package com.geek.security.springmvc.model;

import lombok.Data;

@Data
public class AuthenticationRequest {
    // 认证请求的参数。

    private String username;

    private String password;
}

package com.geek.security.springmvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UserDto {
    // 用户身份信息。
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

  • service 层。
package com.geek.security.springmvc.service.impl;

import com.geek.security.springmvc.model.AuthenticationRequest;
import com.geek.security.springmvc.model.UserDto;
import com.geek.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 {

    // 用户信息。
    private Map<String, UserDto> userMap = new HashMap<>();

    {
        Set<String> authorities1 = new HashSet<>();
        authorities1.add("p1");
        Set<String> authorities2 = new HashSet<>();
        authorities2.add("p2");

        userMap.put("zhangsan", new UserDto("1010", "zhangsan", "123", "张三", "133111", authorities1));
        userMap.put("lisi", new UserDto("1011", "lisi", "456", "李四", "134553", authorities2));
    }

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

    /**
     * 认证信息。校验用户身份是否合法。
     *
     * @param authenticationRequest 用户认证请求。账号和密码。
     * @return 认证成功的用户信息。
     */
    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
        // 校验参数是否为空。
        if (authenticationRequest == null
                || StringUtils.isEmpty(authenticationRequest.getUsername())
                || StringUtils.isEmpty(authenticationRequest.getPassword())) {

            throw new RuntimeException("账号或密码为空。");
        }

        // 根据账号查询用户。(模拟)。
        UserDto userDto = getUserDto(authenticationRequest.getUsername());

        // 判断用户是否为空。
        if (userDto == null) {
            throw new RuntimeException("查询不到该用户。");
        }

        // 校验密码。
        if (!authenticationRequest.getPassword().equals(userDto.getPassword())) {
            throw new RuntimeException("账号或密码错误");
        }

//        return null;
        // 认证通过。返回用户身份信息。
        return userDto;
    }

}

  • controller 层。
package com.geek.security.springmvc.controller;

import com.geek.security.springmvc.model.AuthenticationRequest;
import com.geek.security.springmvc.model.UserDto;
import com.geek.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {

    @Autowired
    private AuthenticationService authenticationService;

    @RequestMapping(value = "/login", produces = "text/plain;charset=utf-8")// 返回纯文本。
    public String login(AuthenticationRequest authenticationRequest) {
        UserDto userDto = authenticationService.authentication(authenticationRequest);
        return userDto.getUsername() - " 登录成功。";
    }
}

访问 http://localhost:8080/security_springmvc/,自动跳转到 login.jsp。



实现会话功能~session。

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

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

增加会话控制。

首先在 UserDao 中定义一个 SESSION_USER_KEY,作为 session 中存放登录用户信息的 key。
HttpSession API。

package com.geek.security.springmvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UserDto {
    // 存放登录用户信息的 key。
    public static final String SESSION_USER_KEY = "_user";
    // 用户身份信息。
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

LoginCotroller 中,登录成功后加入 session。
package com.geek.security.springmvc.controller;

import com.geek.security.springmvc.model.AuthenticationRequest;
import com.geek.security.springmvc.model.UserDto;
import com.geek.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

@RestController
public class LoginController {

    @Autowired
    private 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);

        return userDto.getUsername() - "登录成功。";
    }
}

测试 getSession();。
package com.geek.security.springmvc.controller;

import com.geek.security.springmvc.model.AuthenticationRequest;
import com.geek.security.springmvc.model.UserDto;
import com.geek.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
    private AuthenticationService authenticationService;

    /**
     * 登录功能。
     *
     * @param authenticationRequest
     * @param session
     * @return
     */
    @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);

        return userDto.getUsername() - "登录成功。";
    }

    /**
     * 测试资源 1。
     *
     * @param session
     * @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";
    }
}

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


实现授权功能。

写两个测试资源接口。

增加退出功能。

package com.geek.security.springmvc.controller;

import com.geek.security.springmvc.model.AuthenticationRequest;
import com.geek.security.springmvc.model.UserDto;
import com.geek.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
    private AuthenticationService authenticationService;

    /**
     * 登录功能。
     *
     * @param authenticationRequest
     * @param session
     * @return
     */
    @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);

        return userDto.getUsername() - " 登录成功。";
    }

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

    /**
     * 测试资源 1。
     *
     * @param session
     * @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";
    }

    /**
     * 测试资源 2。
     *
     * @param session
     * @return
     */
    @GetMapping(value = "r/r2", produces = {"text/plain; charset=utf-8"})
    public String r2(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 - " 访问资源 r2";
    }
}

增加:用户权限字段。
package com.geek.security.springmvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

@Data
@AllArgsConstructor
public class UserDto {
    // 存放登录用户信息的 key。
    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;
}



Service 层添加权限字段。
@Service
public class AuthenticationServiceImpl implements AuthenticationService {

    // 用户信息。
    private Map<String, UserDto> userMap = new HashMap<>();

    {
        Set<String> authorities1 = new HashSet<>();
        authorities1.add("p1");
        Set<String> authorities2 = new HashSet<>();
        authorities2.add("p2");

        userMap.put("zhangsan", new UserDto("1010", "zhangsan", "123", "张三", "133111", authorities1));
        userMap.put("lisi", new UserDto("1011", "lisi", "456", "李四", "134553", authorities2));
    }


使用拦截器实现授权。

在 interceptor 包下定义 SimpleAuthenticationInterceptor 拦截器,实现授权拦截。

package com.geek.security.springmvc.interceptor;

import com.geek.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 {
    /**
     * 校验用户请求的 url 是否在用户的权限范围内。
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取用户身份信息。(从 Session 中获取)。
        Object obj = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
        if (obj == null) {
            // 没有认证,提示登录。// 匿名访问就失效了。
            writeContent(response, "请登录。");
        }
        UserDto userDto = (UserDto) obj;
        // 请求的 url。
        String requestURI = request.getRequestURI();

        // todo。如果没有登录,这里会报空指针异常。
        if (userDto.getAuthorities().contains("p1") && requestURI.contains("/r/r1")) {
            return true;// 放行。
        }
        if (userDto.getAuthorities().contains("p2") && requestURI.contains("/r/r2")) {
            return true;// 放行。
        }
        writeContent(response, "没有权限。");

        return false;
    }

    private void writeContent(HttpServletResponse response, String s) throws IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.println(s);
        writer.close();
//        response.resetBuffer();
    }

}



WebConfig 中添加拦截器。
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    // 配置拦截器。
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");// 指定。为了不拦截登录页面。
    }
package com.geek.security.springmvc.config;

import com.geek.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.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration// SpringMVC.xml。
@EnableWebMvc
@ComponentScan(basePackages = "com.geek.security.springmvc",
        includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)}
)
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    // 配置拦截器。
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");// 指定。为了不拦截登录页面。
    }

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

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
    }
}

  • 测试。

http://localhost:8080/security-springmvc/r/r1
张三 访问资源 r1

http://localhost:8080/security-springmvc/r/r2
没有权限。

http://localhost:8080/security-springmvc/r/r2
李四 访问资源 r2



修改 service 层,增加权限检测。
package com.geek.security.springmvc.service.impl;

import com.geek.security.springmvc.model.AuthenticationRequest;
import com.geek.security.springmvc.model.UserDto;
import com.geek.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 {

    // 用户信息。
    private Map<String, UserDto> userMap = new HashMap<>();

    {

        Set<String> authorities1 = new HashSet<>();
        authorities1.add("p1");
        Set<String> authorities2 = new HashSet<>();
        authorities2.add("p2");

        userMap.put("zhangsan", new UserDto("1010", "zhangsan", "123", "张三", "133111", authorities1));
        userMap.put("lisi", new UserDto("1011", "lisi", "456", "李四", "134553", authorities2));
    }

    // 模拟用户查询。
    public UserDto getUserDto(String username) {
        return userMap.get(username);
    }

    /**
     * 认证信息。校验用户身份是否合法。
     *
     * @param authenticationRequest 用户认证请求。账号和密码。
     * @return 认证成功的用户信息。
     */
    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
        // 校验参数是否为空。
        if (authenticationRequest == null
                || StringUtils.isEmpty(authenticationRequest.getUsername())
                || StringUtils.isEmpty(authenticationRequest.getPassword())) {

            throw new RuntimeException("账号或密码为空。");
        }

        // 根据账号查询用户。
        UserDto userDto = getUserDto(authenticationRequest.getUsername());

        // 判断用户是否为空。
        if (userDto == null) {
            throw new RuntimeException("查询不到该用户。");
        }

        // 校验密码。
        if (!authenticationRequest.getPassword().equals(userDto.getPassword())) {
            throw new RuntimeException("账号或密码错误");
        }

//        return null;
        // 认证通过。返回用户身份信息。
//        return userDto;
    }

}



SpringMVC.xml 配置拦截器。
package com.geek.security.springmvc.config;

import com.geek.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.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration// SpringMVC.xml。
@EnableWebMvc
@ComponentScan(basePackages = "com.geek.security.springmvc",
        includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)}
)
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    // 配置拦截器。
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(simpleAuthenticationInterceptor);
    }

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

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login").addPathPatterns("/r/**");
    }
}

zhangsan 有权限访问 http://localhost:8080/security_springmvc/r/r1。
lisi 有权限访问 http://localhost:8080/security_springmvc/r/r2。



Spring Security。

what。

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements



Maven~环境搭建。
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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.geek</groupId>
    <artifactId>spring-security-00</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>
        <!-- Spring Security。-->
        <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-web -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <!-- Spring Security。end。-->

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

    <build>
        <!--<pluginManagement>-->
        <plugins>
            <!--<plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat6-maven-plugin</artifactId>
                <version>2.2</version>
            </plugin>-->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
            </plugin>
        </plugins>
        <!--</pluginManagement>-->
    </build>

</project>

Spring 容器配置。
package com.geek.security.spring.config;

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

@Configuration// applicationContext.xml。
@ComponentScan(basePackages = "com.geek.security.spring",
        excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)}
        // 排除 web 层 Controller。
)
public class ApplicationConfig {

    // 在此配置除了 Controller 的其他 Bean。eg. 数据库连接池,事务管理器,业务 Bean 等。
}

servletContext 配置。

// Spring Security 框架提供了拦截器,就不用我们自己写了。

package com.geek.security.spring.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.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration// SpringMVC.xml。
@EnableWebMvc
@ComponentScan(basePackages = "com.geek.security.spring",
        includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)}
)
public class WebConfig implements WebMvcConfigurer {
/*
    @Autowired
    private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    // 配置拦截器。
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");// 指定。为了不拦截登录页面。
    }
*/
// Spring Security 框架提供了拦截器,就不用我们自己写了。

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

    // 默认 url 根路径跳转到 /login。此 url 为 Spring Security 提供。
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }
}



加载 Spring 容器。

不变。

package com.geek.security.spring.init;

import com.geek.security.spring.config.ApplicationConfig;
import com.geek.security.spring.config.WebConfig;
import com.geek.security.spring.config.WebSecurityConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /*
    public interface WebApplicationInitializer {
        void onStartup(ServletContext var1) throws ServletException;
    }
    只要最终实现了此接口,就能在容器启动时执行类中的方法。
    */

    // 加载 Spring 容器。(applicationContext.xml)。
    @Override
    protected Class<?>[] getRootConfigClasses() {
//        return new Class[0];
        return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
    }

    // 加载 SpringMVC.xml。servletContext。
    @Override
    protected Class<?>[] getServletConfigClasses() {
//        return new Class[0];
        return new Class[]{WebConfig.class};
    }

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

在这里插入图片描述

SpringApplicationInitializer 相当于 web.xml。使用 Servlet 3.0 开发则不再需要定义 web.xml。
Class ApplicationConfig 对应 application-context.xml。
Class WebConfig 对应 spring-mnv.xml。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <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/application-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>

  • WebConfig.java 中添加。
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
    }


认证。
认证页面。

SpringSecurity 提供默认认证页面。不用我们自己写。



安全配置。

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

  • 在 config 包下定义 WebSecurityConfig ——> 用户信息、密码编码器,安全拦截机制。
package com.geek.security.spring.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;

/**
 * 安全配置。
 * 用户信息、密码编码器、安全拦截机制。
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 定义用户信息服务(查询用户信息)。查询数据库或内存方式。
    @Bean
    public UserDetailsService userDetailsService() {
        // 基于内存的方式。
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return inMemoryUserDetailsManager;
    }

    // 密码编码器。(密码比对方式)。
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 不加密。
        return NoOpPasswordEncoder.getInstance();
    }

    // 安全拦截机制。(关键)。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.authorizeRequests()
                .antMatchers("/r/**").authenticated()// 此路径 /r/** 要求认证。
                .anyRequest().permitAll()// 除了 /r/**,其他放行。
                .and()
                .formLogin()// 允许表单登录。
                .successForwardUrl("/login-success");// 自定义登录成功的页面地址。
    }
}

  • 放入 Spring 容器加载。
    // 加载 Spring 容器。(applicationContext.xml)。
    @Override
    protected Class<?>[] getRootConfigClasses() {
//        return new Class[0];
        return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
    }
package com.geek.security.spring.init;

import com.geek.security.spring.config.ApplicationConfig;
import com.geek.security.spring.config.WebConfig;
import com.geek.security.spring.config.WebSecurityConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /*
    public interface WebApplicationInitializer {
        void onStartup(ServletContext var1) throws ServletException;
    }
    只要最终实现了此接口,就能在容器启动时执行类中的方法。
    */

    // 加载 Spring 容器。(applicationContext.xml)。
    @Override
    protected Class<?>[] getRootConfigClasses() {
//        return new Class[0];
        return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
    }

    // 加载 SpringMVC.xml。servletContext。
    @Override
    protected Class<?>[] getServletConfigClasses() {
//        return new Class[0];
        return new Class[]{WebConfig.class};
    }

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



Spring Security 初始化。
  • 若当前环境没有使用 Spring 或 Spring MVC,则需要将 WebSecurityConfig(Spring Security 配置类) 传入超类,以确保获取配置,并创建 Spring Context。
  • 相反,若当前环境已使用 Spring,我们应该在现有的 Spring Context 中注册 Spring Security(上一步已将 WebSecurityConfig 加载至 rootcontext),此方法可以什么都不做。

在 init 包下定义 SpringSecurityApplicationInitializer。

package com.geek.security.init;

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

public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    public SpringSecurityApplicationInitializer() {
        // 若当前没有使用 Spring 或 Spring MVC。
//        super(WebSecurityConfig.class);
    }
}



默认根路径请求。
package com.geek.security.spring.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.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration// SpringMVC.xml。
@EnableWebMvc
@ComponentScan(basePackages = "com.geek.security.spring",
        includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)}
)
public class WebConfig implements WebMvcConfigurer {
/*
    @Autowired
    private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    // 配置拦截器。
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");// 指定。为了不拦截登录页面。
    }
*/
// Spring Security 框架提供了拦截器,就不用我们自己写了。

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

    // 默认 url 根路径跳转到 /login。此 url 为 Spring Security 提供。
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }
}



认证成功页面。

/login-success

    // 安全拦截机制。(关键)。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.authorizeRequests()
                .antMatchers("/r/**").authenticated()// 此路径 /r/** 要求认证。
                .anyRequest().permitAll()// 除了 /r/**,其他放行。
                .and()
                .formLogin()// 允许表单登录。
                .successForwardUrl("/login-success");// 自定义登录成功的页面地址。
    }

在 c
ontroller 中配置。

package com.geek.security.spring.controller;

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

@RestController
public class LoginController {

    @RequestMapping(value = "/login-success", produces = {"text/plain; charset=UTF-8"})
    public String loginSuccess() {
        return "登录成功。";
    }
}



测试。

http://localhost:8080/security_springmvc/

package com.geek.security.spring.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 {

    @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。";
    }
}

Spring Security 5.0.2.RELEASE 版本测试 logout 出现 404。



授权。
package com.geek.security.spring.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;

/**
 * 安全配置。
 * 用户信息、密码编码器、安全拦截机制。
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 定义用户信息服务(查询用户信息)。查询数据库或内存方式。
    @Bean
    public UserDetailsService userDetailsService() {
        // 基于内存的方式。
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return inMemoryUserDetailsManager;
    }

    // 密码编码器。(密码比对方式)。
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 不加密。
        return NoOpPasswordEncoder.getInstance();
    }

    // 安全拦截机制。(关键)。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.authorizeRequests()
                
                // 拦截实现授权。
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")
                
                .antMatchers("/r/**").authenticated()// 此路径 /r/** 要求认证。
                .anyRequest().permitAll()// 除了 /r/**,其他放行。
                .and()
                .formLogin()// 允许表单登录。
                .successForwardUrl("/login-success");// 自定义登录成功的页面地址。
    }
}

如果没有权限。会报 403。

在这里插入图片描述



Spring Boot 集成 Spring Security。

  • 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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.geek</groupId>
    <artifactId>security-spring-boot</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.13.RELEASE</version>
    </parent>


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

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

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

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

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

  • application.yml
server:
  port: 8080
  servlet:
    context-path: /security-springboot
spring:
  application:
    name: security-springboot


WebConfig。
package com.geek.security.springboot.config;

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

@Configuration
public class WebConfig implements WebMvcConfigurer {

    // Spring Boot 的自动装配机制,不需要 @EnableWebMvc 和 @ComponentScan。

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }
}



视图解析器配置在 application.properties 中。
spring:
  mvc:
    view:
      prefix: /WEB-INF/views/
      suffix: .jsp


安全配置。
package com.geek.security.springboot.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;

/**
 * 安全配置。
 * 用户信息、密码编码器、安全拦截机制。
 */
//@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 定义用户信息服务(查询用户信息)。查询数据库或内存方式。
    @Bean
    public UserDetailsService userDetailsService() {
        // 基于内存的方式。
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return inMemoryUserDetailsManager;
    }

    // 密码编码器。(密码比对方式)。
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 不加密。
        return NoOpPasswordEncoder.getInstance();
    }

    // 安全拦截机制。(关键)。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.authorizeRequests()

                // 拦截实现授权。
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")

                .antMatchers("/r/**").authenticated()// 此路径 /r/** 要求认证。
                .anyRequest().permitAll()// 除了 /r/**,其他放行。
                .and()
                .formLogin()// 允许表单登录。
                .successForwardUrl("/login-success");// 自定义登录成功的页面地址。
    }
}

  • controller。
package com.geek.security.springboot.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 {

    @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。";
    }
}



工作原理。

结构总览。

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

当初始化 Spring Security 时,会创建一个名为 SpringSecurityFilterChain 的 Servlet 过滤器,类型为 org.springframework.security.web.FilterChainProxy,ta 实现了 javax.servlet.Filter,因此外部的请求会经过此类。

在这里插入图片描述

FilterChainProxy 是一个代理,真正的作用是 FilterChainProxy 中 SecurityFilterChain 所包含的各个 Filter,同时这些 Filter 作为 Bean 被 Spring 管理,他们是 Spring Security 的核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把他们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理。

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

  • SecurityContextPersistenceFilter。

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

  • UsernamePasswordAuthenticationFilter。

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

  • FilterSecuritylnterceptor。

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

  • ExceptionTranslationFilter。

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



认证流程。

在这里插入图片描述



AuthenticationProvider。全局调度者。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {

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

可以自定义。

package com.geek.security.springboot.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;

@Service
public class SpringDataUserDetailService implements UserDetailsService {

    // 根据账号查询用户信息。
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 将来连接数据库根据账号查询用户信息。
        // 暂时采用模拟方式。
        // 登录账号。
        System.out.println("username = " - username);
        UserDetails userDetails = User.withUsername("zhangsan").password("123").authorities("p1").build();

        return userDetails;
    }
}

package com.geek.security.springboot.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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 安全配置。
 * 用户信息、密码编码器、安全拦截机制。
 */
//@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 定义用户信息服务(查询用户信息)。查询数据库或内存方式。
/*
    @Bean
    public UserDetailsService userDetailsService() {
        // 基于内存的方式。
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return inMemoryUserDetailsManager;
    }
*/

    // 密码编码器。(密码比对方式)。
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 不加密。
        return NoOpPasswordEncoder.getInstance();
    }

    // 安全拦截机制。(关键)。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.authorizeRequests()

                // 拦截实现授权。
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")

                .antMatchers("/r/**").authenticated()// 此路径 /r/** 要求认证。
                .anyRequest().permitAll()// 除了 /r/**,其他放行。
                .and()
                .formLogin()// 允许表单登录。
                .successForwardUrl("/login-success");// 自定义登录成功的页面地址。
    }
}



PasswordEcoder。
    // 密码编码器。(密码比对方式)。
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
/*
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 不加密。
        return NoOpPasswordEncoder.getInstance();
    }
*/
  • 测试密码加密与校验。
package com.geek.security.springboot;

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 testBCrypt() {
        // 对密码进行加密。
        String hashpw = BCrypt.hashpw("123", BCrypt.gensalt());

        // hashpw = $2a$10$ZV2gne5gM44.eRTi0KFOK.MJ/OmjB3h6Aw1sUk9YoOU8rbSUHtYwG
        // hashpw = $2a$10$CKaHgxPgt0WU3SWihmSR6uOzTURqJqH8jkZ/xLGikfm2KBj1E76Jy

        System.out.println("~ ~ ~ ~ ~ ~ ~");
        System.out.println("hashpw = " - hashpw);
        System.out.println("~ ~ ~ ~ ~ ~ ~");

        // 校验密码。
        boolean checkpw = BCrypt.checkpw("123", "$2a$10$ZV2gne5gM44.eRTi0KFOK.MJ/OmjB3h6Aw1sUk9YoOU8rbSUHtYwG");
        System.out.println("checkpw = " - checkpw);
        boolean checkpw1 = BCrypt.checkpw("123", "$2a$10$CKaHgxPgt0WU3SWihmSR6uOzTURqJqH8jkZ/xLGikfm2KBj1E76Jy");
        System.out.println("checkpw1 = " - checkpw1);
    }
}



授权流程。

在这里插入图片描述

  • public interface AccessDecisionManager {

在这里插入图片描述

  • 投票。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.security.access.vote;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;

public class AffirmativeBased extends AbstractAccessDecisionManager {
    public AffirmativeBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {
        super(decisionVoters);
    }

    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        int deny = 0;
        Iterator var5 = this.getDecisionVoters().iterator();

        while(var5.hasNext()) {
            AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
            int result = voter.vote(authentication, object, configAttributes);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Voter: " - voter - ", returned: " - result);
            }

            switch(result) {
            case -1:
                ++deny;
                break;
            case 1:
                return;
            }
        }

        if (deny > 0) {
            throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        } else {
            this.checkAllowIfAllAbstainDecisions();
        }
    }
}



自定义登录页面。
package com.geek.security.springboot.config;

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

@Configuration
public class WebConfig implements WebMvcConfigurer {

    // Spring Boot 的自动装配机制,不需要 @EnableWebMvc 和 @ComponentScan。

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login-view");
        registry.addViewController("/login-view").setViewName("login");
    }
}

package com.geek.security.springboot.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 安全配置。
 * 用户信息、密码编码器、安全拦截机制。
 */
//@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 密码编码器。(密码比对方式)。
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 安全拦截机制。(关键)。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.csrf().disable()// 屏蔽 CSRF 控制。让 Spring Security 不再限制 CSRF。
                .authorizeRequests()
                // 拦截实现授权。
//                .antMatchers("/r/r1").hasAuthority("p1")
//                .antMatchers("/r/r2").hasAuthority("p2")

                .antMatchers("/r/**").authenticated()// 此路径 /r/** 要求认证。
                .anyRequest().permitAll()// 除了 /r/**,其他放行。
                .and()
                .formLogin()// 允许表单登录。
                .loginPage("/login-view")// 指定我们自己的登录页面,Spring Security 以重定向的方式跳转到 /login-view。
                .loginProcessingUrl("/login")// 指定登录处理的 url,也就是用户名、密码表单提交的目的路径。
                .successForwardUrl("/login-success")// 自定义登录成功的页面地址。
                .permitAll();
    }
}



CSRF。
  • 方法一。
//        super.configure(http);
        http.csrf().disable()// 屏蔽 CSRF 控制。让 Spring Security 不再限制 CSRF。
                .authorizeRequests()
  • 方法二。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>用户登录。</title>
</head>
<body>

<form action="login" method="post">

    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">

    用户名:<input type="text" name="username"><hr>

<br>&nbsp;&nbsp;&nbsp;码:<input type="password" name="password"> <hr>

<br>
    <input type="submit" value="登录">
</form>

</body>
</html>



连接数据库认证。
CREATE SCHEMA `user_db` DEFAULT CHARACTER SET utf8 ;

CREATE TABLE `user_db`.`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) NULL DEFAULT NULL COMMENT '手机号',
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
ROW_FORMAT = DYNAMIC;

ALTER TABLE `user_db`.`t_user` 
CHANGE COLUMN `id` `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户 id。' ;


package com.geek.security.springboot.model;

import lombok.Data;

@Data
public class UserDto {

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

}

  • service 层。
package com.geek.security.springboot.service;

import com.geek.security.springboot.dao.UserDao;
import com.geek.security.springboot.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;

@Service
public class SpringDataUserDetailService implements UserDetailsService {

    @Autowired
    private UserDao userDao;


    // 根据账号查询用户信息。
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 将来连接数据库根据账号查询用户信息。
        UserDto userDto = userDao.getUserByUsername(username);
        if (userDto == null) {
            // 如果用户查不到,返回 null,由 provider 来抛出异常。
            return null;
        }
        // 登录账号。
        System.out.println("username = " - username);
        // 暂时采用模拟方式。
//        UserDetails userDetails = User.withUsername("zhangsan").password("$2a$10$ZV2gne5gM44.eRTi0KFOK.MJ/OmjB3h6Aw1sUk9YoOU8rbSUHtYwG").authorities("p1").build();

        UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities("p1").build();

        return userDetails;
    }
}



会话。
    /**
     * 获取当前用户信息。
     *
     * @return
     */
    private String getUsername() {

        String username = null;

        // 当前登陆成功的用户身份。
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 用户身份。
        Object principal = authentication.getPrincipal();
        if (principal == null) {
            username = "匿名";
        }
        if (principal instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) principal;
            username = userDetails.getUsername();
        } else {
            username = principal.toString();
        }

        return username;
    }


会话何时创建。
  • ALWAYS。
    如果没有 session 就创建一个。

  • IF_REQUIRED。
    如果需要就创建一个 Session。默认登录时。

  • NEVER。
    Spring Security 将不会创建 Session。但是若果应用中其他地方创建了 Session,那么 Spring Security 将会使用 ta。

  • STATELESS。
    Spring Security 将绝对不会创建 Session,也不使用 Session。

在这里插入图片描述

http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)// 会话何时创建。


自定义退出。
http
                // 自定义退出。
                .and()
                .logout()// 提供系统退出支持。
                .logoutUrl("/logout")// 设置触发退出操作的 url。默认是 /logout。
                .logoutSuccessUrl("/logout-view?logout")// 退出之后跳转的 url。默认是 /logout-view?logout。 
//                .logoutSuccessHandler(logoutSuccessHandler)// 定制的 LogoutSuccessHandler。用于实现用户退出成功时的处理。如果指定了这个选项那么 .logoutSuccessUrl() 的设置会被覆盖。 
//                .addLogoutHandler(logoutSuccessHandler)// 添加一个 LogoutHandler,用于实现用户退出时的清理工作。默认 SecurityContextLogoutHandler 会被添加为最后一个 LogoutHandler。
                .invalidateHttpSession(true)// 指定是否在退出时让 HttpSession 无效。默认为 true。

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

        // 当退出操作执行时。
        // 使 HTTP Session 无效。
        // 清除 SecurityContextHolder。
        // 跳转到 “/logout-view?logout”。
        ;



授权。

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

  • 角色表。
CREATE TABLE `user_db`.`t_role` (
  `id` VARCHAR(32) NOT NULL,
  `role_name` VARCHAR(255) NULL DEFAULT NULL,
  `description` VARCHAR(255) NULL DEFAULT NULL,
  `create_time` DATETIME NULL DEFAULT NULL,
  `update_time` DATETIME NULL DEFAULT NULL,
  `status` CHAR(1) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `role_name_UNIQUE` (`role_name` ASC))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

INSERT INTO `user_db`.`t_role` (`id`, `role_name`, `status`) VALUES ('1', '管理员', ' ');

  • 用户角色关系表。
CREATE TABLE `user_db`.`t_user_role` (
  `user_id` VARCHAR(32) NOT NULL,
  `role_id` VARCHAR(32) NOT NULL,
  `create_time` DATETIME NULL DEFAULT NULL,
  `creator` VARCHAR(255) NULL DEFAULT NULL,
  PRIMARY KEY (`user_id`, `role_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

INSERT INTO `user_db`.`t_user_role` (`user_id`, `role_id`) VALUES ('1', '1');

  • 权限表。
CREATE TABLE `user_db`.`t_permission` (
  `id` VARCHAR(32) NOT NULL,
  `code` VARCHAR(32) NOT NULL COMMENT '权限标识符。',
  `description` VARCHAR(64) NULL DEFAULT NULL COMMENT '描述。',
  `url` VARCHAR(128) NULL DEFAULT NULL COMMENT '请求地址。',
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

INSERT INTO `user_db`.`t_permission` (`id`, `code`, `description`, `url`) VALUES ('1', 'p1', '测试资源1', '/r/r1');
INSERT INTO `user_db`.`t_permission` (`id`, `code`, `description`, `url`) VALUES ('2', 'p3', '测试资源2', '/r/r2');

  • 角色权限关系表。
CREATE TABLE `user_db`.`t_role_permission` (
  `role_id` VARCHAR(32) NOT NULL,
  `permission_id` VARCHAR(32) NOT NULL,
  PRIMARY KEY (`role_id`, `permission_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

INSERT INTO `user_db`.`t_role_permission` (`role_id`, `permission_id`) VALUES ('1', '1');
INSERT INTO `user_db`.`t_role_permission` (`role_id`, `permission_id`) VALUES ('1', '2');

package com.geek.security.springboot.model;

import lombok.Data;

@Data
public class PermissionDto {

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

package com.geek.security.springboot.dao;

import com.geek.security.springboot.model.PermissionDto;
import com.geek.security.springboot.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
    private JdbcTemplate jdbcTemplate;

    /**
     * 根据用户 id 查询用户权限。
     *
     * @param userId
     * @return
     */
    public List<String> findPermissionsByUserId(String userId) {
        String sql = "SELECT \n" +
                "    *\n" +
                "FROM\n" +
                "    t_permission\n" +
                "WHERE\n" +
                "    id IN (SELECT \n" +
                "            permission_id\n" +
                "        FROM\n" +
                "            t_role_permission\n" +
                "        WHERE\n" +
                "            role_id IN (SELECT \n" +
                "                    role_id\n" +
                "                FROM\n" +
                "                    t_user_role\n" +
                "                WHERE\n" +
                "                    user_id = ?));";
        List<PermissionDto> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionDto.class));
        List<String> permissionList = new ArrayList<>();
        list.forEach(c -> permissionList.add(c.getCode()));
        return permissionList;
    }

    /**
     * 根据账号查询用户信息。
     *
     * @param username
     * @return
     */
    public UserDto getUserByUsername(String username) {
        String sql = "select id, username, password, fullname, mobile from t_user where username = ?";
        List<UserDto> userDtoList = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));
        if (userDtoList != null && userDtoList.size() == 1) {
            return userDtoList.get(0);
        }
        return null;
    }
}

package com.geek.security.springboot.service;

import com.geek.security.springboot.dao.UserDao;
import com.geek.security.springboot.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;

@Service
public class SpringDataUserDetailService implements UserDetailsService {

    @Autowired
    private UserDao userDao;

    // 根据账号查询用户信息。
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 将来连接数据库根据账号查询用户信息。
        UserDto userDto = userDao.getUserByUsername(username);
        if (userDto == null) {
            // 如果用户查不到,返回 null,由 provider 来抛出异常。
            return null;
        }

        // 根据用户 id 查询用户的权限。
        List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
        // 将 permissions 转为数组。
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);

        UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray).build();


        // 登录账号。
        System.out.println("username = " - username);
        // 暂时采用模拟方式。
//        UserDetails userDetails = User.withUsername("zhangsan").password("$2a$10$ZV2gne5gM44.eRTi0KFOK.MJ/OmjB3h6Aw1sUk9YoOU8rbSUHtYwG").authorities("p1").build();

//        UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities("p1").build();

        return userDetails;
    }
}



web 授权。
package com.geek.security.springboot.config;

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

/**
 * 安全配置。
 * 用户信息、密码编码器、安全拦截机制。
 */
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 定义用户信息服务(查询用户信息)。查询数据库或内存方式。
/*
    @Bean
    public UserDetailsService userDetailsService() {
        // 基于内存的方式。
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return inMemoryUserDetailsManager;
    }
*/

    // 密码编码器。(密码比对方式)。
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

/*
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 不加密。
        return NoOpPasswordEncoder.getInstance();
    }
*/

    // 安全拦截机制。(关键)。
/*
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http
                .authorizeRequests()
                // 拦截实现授权。
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()// 此路径 /r/** 要求认证。
                .anyRequest().permitAll()// 除了 /r/**,其他放行。
                .and()
                .formLogin()// 允许表单登录。
                .successForwardUrl("/login-success");// 自定义登录成功的页面地址。
    }
*/

    // 安全拦截机制。(关键)。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.csrf().disable()// 屏蔽 CSRF 控制。让 Spring Security 不再限制 CSRF。
                .authorizeRequests()

//                .antMatchers("/admin/**").hasRole("ADMIN")// 这里如果通过了,后面就不再执行。
//                .antMatchers("/admin/login").permitAll()
                // ↓ ↓ ↓ 顺序很重要。具体放前面。

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

                // 拦截实现授权。
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()// 此路径 /r/** 要求认证。
                .anyRequest().permitAll()// 除了 /r/**,其他放行。
                .and()
                .formLogin()// 允许表单登录。
//                .loginPage("/login-view")// 指定我们自己的登录页面,Spring Security 以重定向的方式跳转到 /login-view。
//                .loginPage("/login")// 指定我们自己的登录页面,Spring Security 以重定向的方式跳转到 /login-view。
//                .loginProcessingUrl("/login")// 指定登录处理的 url,也就是用户名、密码表单提交的目的路径。
                .successForwardUrl("/login-success")// 自定义登录成功的页面地址。
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)// 会话何时创建。
                // 自定义退出。
                .and()
                .logout()// 提供系统退出支持。
                .logoutUrl("/logout")// 设置触发退出操作的 url。默认是 /logout。
                .logoutSuccessUrl("/logout-view?logout")// 退出之后跳转的 url。默认是 /logout-view?logout。
//                .logoutSuccessHandler(logoutSuccessHandler)// 定制的 LogoutSuccessHandler。用于实现用户退出成功时的处理。如果指定了这个选项那么 .logoutSuccessUrl() 的设置会被覆盖。
//                .addLogoutHandler(logoutSuccessHandler)// 添加一个 LogoutHandler,用于实现用户退出时的清理工作。默认 SecurityContextLogoutHandler 会被添加为最后一个 LogoutHandler。
                .invalidateHttpSession(true)// 指定是否在退出时让 HttpSession 无效。默认为 true。

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

        // 当退出操作执行时。
        // 使 HTTP Session 无效。
        // 清除 SecurityContextHolder。
        // 跳转到 “/logout-view?logout”。
        ;
    }
}

注意。
//                .antMatchers("/admin/**").hasRole("ADMIN")// 这里如果通过了,后面就不再执行。
//                .antMatchers("/admin/login").permitAll()
                // ↓ ↓ ↓ 顺序很重要。具体放前面。


方法授权。

Spring Security 2.0 开始,支持服务层方法的安全性支持。

可以在任何 @Configuration 实例上使用 @EnableGlobalMethodSecurity 注解来启用基于注解的安全性。

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@PreAuthorize。
@PostAuthorize。
@Secured。
    /**
     * 测试资源 1。
     *
     * @return
     */
    @GetMapping(value = "/r/r1", produces = {"text/plain; charset=UTF-8"})
//    @PreAuthorize("isAnonymous()")
    @PreAuthorize("hasAuthority('p1')")// 拥有 p1 权限才可以访问。
    //    @PreAuthorize("hasAnyAuthority('p1', 'p2')")
    @PreAuthorize("hasAuthority('p_transfer') and hasAnyAuthority('p_read-account')")
    public String r1() {
        return getUsername() - "访问资源 1。";
    }



分布式系统认证方案。

随软件环境和需求的变化,软件的架构由单体结构演变为分布式架构,具有分布式架构的系统叫分布式系统,分布式系统的运行通常依赖网络,ta 将单体结构的系统分为若干服务,服务之间诵过网络交互来完成用户的业务处理,当前流行的微服务架构就是分布式系统架构,如下图。

在这里插入图片描述
分布式系统具体如下基本恃点。

每个部分都可以独立部罟,服务之间交互通过网络进行通信,比如:订单服务、商品服务。

  • 分布性
  • 伸缩性。每个部分都可以集群方式部署,并可针对部分结点进行硬件及软件扩容,具有一定的伸缩能力。
  • 共享性。每个部分都可以作为共享资源对外提供服务,多个部分可能有操作共享资源的情况。
  • 开放性。每个部分根据需求都可以对外发布共享资源的访问接囗,并可允许第三方系统访问。


分布式认证需求。

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

  • 统一认证授权。

提供独立的认证服务,统一处理认证授权。
无论是不同类型的用户,还是不同种类的客户端(web 端,H5、APP),均采用一致的认证、权限、会话机制,实现统一认证授权。
要实现统一则认证方式必须可扩展,支持各种认证需求,比如:用户名密码认证、短信验证码、二维码、人脸识别等认证方式,并可以非常灵活的切换。

  • 应用接入认证。

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



选型分析。
基于 Session 的认证方式。

在分布式的环境下,基于 session 的认证会出现一个问题,每个应用服务都需要在 session 中存储用户身份信息,通过负载均衡奖本地的请求分配到另一个应用服务需要将 session 信息带过去,否则会重新认证。

在这里插入图片描述

  • Session 复制。

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

  • Session 粘贴。

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

  • Session 集中存储。

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

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



基于 token 的认证方式。

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

在这里插入图片描述
根据选型的分析,决定采用基于 token 的认证方式。ta 的优点是。

  • 适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。
  • token 认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议、Oauth2.0 等。
  • 一般情况服务端无需存储会话信息,减轻了服务端的压力。

分布式系统认证技术方案。

在这里插入图片描述



OAuth 2.0。

OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数的所有内容。OAuth 2.0 是 OAuth 协议的延续版本,但不向后兼容 OAuth 1.0。即完全废止了 OAuth 1.0 。很多大公司如 Google, Yahoo, Microsoft 等都提供了 OAuth 认证服务,这些都足以说明 OAUTH 标准逐渐成为放资源授权的标准。
Oauth 协议目前发展到 2.0 版本,1.0 版本过于复杂,2.0 版本已得到广泛应用。

OAuth2.0 是 OAuth 协议的延续版本,但不向前兼容 OAuth 1.0(即完全废止了 OAuth 1.0)。 OAuth 2.0 关注客户端开发者的简易性。要么通过组织在资源拥有者和 HTTP 服务商之间的被批准的交互动作代表用户,要么允许第三方应用代表用户获得访问的权限。同时为 Web 应用,桌面应用和手机,和起居室设备提供专门的认证流程。2012 年 10 月,OAuth 2.0 协议正式发布为 RFC 6749 。

https://baike.baidu.com/item/OAuth2.0/6788617?fr=aladdin

https://oauth.net/2/

下边分析一个 Oauth 2.0 认证的例子, 通过例子去理解 OAuth 2.0 协议的认证流程。本例子是京东网站使用微信认证的过程, 这个过程的简要描述如下。

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



父工程。
<?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>
    <packaging>pom</packaging>
    <modules>
        <module>distributed-security-uaa</module>
        <module>distributed-security-order</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.geek</groupId>
    <artifactId>distributed-security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>distributed-security</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</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.67</version>
            </dependency>

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

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

            <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt -->
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.0.10.RELEASE</version>
            </dependency>

            <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure -->
            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.1.3.RELEASE</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

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

        <!--<dependency>-->
        <!--<groupId>org.projectlombok</groupId>-->
        <!--<artifactId>lombok</artifactId>-->
        <!--<optional>true</optional>-->
        <!--</dependency>-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <!--            <exclusions>
                            <exclusion>
                                <groupId>org.junit.vintage</groupId>
                                <artifactId>junit-vintage-engine</artifactId>
                            </exclusion>
                        </exclusions>-->
        </dependency>

        <!--        <dependency>
                    <groupId>org.springframework.security</groupId>
                    <artifactId>spring-security-test</artifactId>
                    <scope>test</scope>
                </dependency>-->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>



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.geek</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>distributed-security-uaa</artifactId>

    <dependencies>
        <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>

  • 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

package com.geek.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;

@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(basePackages = "com.geek.security.distributed.uaa")
@SpringBootApplication
public class UAA_53020_Application {
    public static void main(String[] args) {
        SpringApplication.run(UAA_53020_Application.class, args);
    }
}



distributed-security-order。
<?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.geek</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>distributed-security-order</artifactId>

    <dependencies>
        <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>

  • 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
#
management.endpoints.web.exposure.include=refresh, health, info, env

#feign.hystrix.enabled=true

  • 启动类。
package com.geek.security.ditributed.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class Order_53021_Application {
    public static void main(String[] args) {
        SpringApplication.run(Order_53021_Application.class, args);
    }
}



授权服务器配置。

@EnableAuthorizationServer 注解

继承 AuthorizationServerConfigurerAdapter
来配置 OAuth 2.0授权服务器。

EnableAuthorizationServer。
package com.geek.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;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    public AuthorizationServer() {
        super();
    }

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

    /**
     * - ClientDetailsServiceConfigurer。
     * 配置客户端详情服务(ClientDetailService)。客户端详情信息在这里初始化。
     * 客户端详情信息直接写死在这里或通过数据库存储调取详情信息。
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        super.configure(clients);
    }

    /**
     * - AuthorizationServerEndpointsConfigurer endpoints。
     * 用来配置令牌(token)的访问端点和令牌服务(token services)。
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
    }
}

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.security.oauth2.config.annotation.web.configuration;

import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    public AuthorizationServerConfigurerAdapter() {
    }

    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    }

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }

    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }
}



客户端详情。

ClientDetailsServiceConfigurer 能够使用内在或者 JDBC 来实现客户端详情服务(ClentDetailsService),ClientDetailsService 负责查找 ClientDetails,而 ClientDetails 有几个重要属性。

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

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

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

import org.springframework.context.annotation.Configuration;
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;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    public AuthorizationServer() {
        super();
    }

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

    /**
     * - ClientDetailsServiceConfigurer。
     * 配置客户端详情服务(ClientDetailService)。客户端详情信息在这里初始化。
     * 客户端详情信息直接写死在这里或通过数据库存储调取详情信息。
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//        super.configure(clients);

        clients.inMemory()// 使用 in-memory 存储。
                .withClient("c1")// clientId
                .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥。
                .resourceIds("res1")
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该 client 允许的授权类型。
                .scopes("all")// 允许的授权范围。
                .autoApprove(false)// false 跳转到授权页面。
                // 验证回调地址。
                .redirectUris("http://www.baidu.com");
    }

    /**
     * - AuthorizationServerEndpointsConfigurer endpoints。
     * 用来配置令牌(token)的访问端点和令牌服务(token services)。
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
    }
}



管理令牌。
package com.geek.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;

@Configuration
public class TokenConfig {

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

package com.geek.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.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.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    public AuthorizationServer() {
        super();
    }

    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService);// 客户端详情服务。
        services.setSupportRefreshToken(true);// 支持刷新令牌。
        services.setTokenStore(tokenStore);// 令牌存储策略。
        services.setAccessTokenValiditySeconds(7200);// 令牌默认有效期 2 小时。
        services.setRefreshTokenValiditySeconds(259200);// 刷新令牌默认有效期 3 天。
        return services;
    }

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

    /**
     * - ClientDetailsServiceConfigurer。
     * 配置客户端详情服务(ClientDetailService)。客户端详情信息在这里初始化。
     * 客户端详情信息直接写死在这里或通过数据库存储调取详情信息。
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//        super.configure(clients);

        clients.inMemory()// 使用 in-memory 存储。
                .withClient("c1")// clientId
                .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥。
                .resourceIds("res1")
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该 client 允许的授权类型。
                .scopes("all")// 允许的授权范围。
                .autoApprove(false)// false 跳转到授权页面。
                // 验证回调地址。
                .redirectUris("http://www.baidu.com");
    }

    /**
     * - AuthorizationServerEndpointsConfigurer endpoints。
     * 用来配置令牌(token)的访问端点和令牌服务(token services)。
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
    }
}



web 安全配置。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    public AuthorizationServer() {
        super();
    }

    // 设置授权码模式的授权码如果存取。
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

model、dao、service。

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

import lombok.Data;

@Data
public class PermissionDto {

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

package com.geek.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.geek.security.distributed.uaa.dao;

import com.geek.security.distributed.uaa.model.PermissionDto;
import com.geek.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
    private JdbcTemplate jdbcTemplate;

    /**
     * 根据用户 id 查询用户权限。
     *
     * @param userId
     * @return
     */
    public List<String> findPermissionsByUserId(String userId) {
        String sql = "SELECT \n" +
                "    *\n" +
                "FROM\n" +
                "    t_permission\n" +
                "WHERE\n" +
                "    id IN (SELECT \n" +
                "            permission_id\n" +
                "        FROM\n" +
                "            t_role_permission\n" +
                "        WHERE\n" +
                "            role_id IN (SELECT \n" +
                "                    role_id\n" +
                "                FROM\n" +
                "                    t_user_role\n" +
                "                WHERE\n" +
                "                    user_id = ?));";
        List<PermissionDto> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionDto.class));
        List<String> permissionList = new ArrayList<>();
        list.forEach(c -> permissionList.add(c.getCode()));
        return permissionList;
    }

    /**
     * 根据账号查询用户信息。
     *
     * @param username
     * @return
     */
    public UserDto getUserByUsername(String username) {
        String sql = "select id, username, password, fullname, mobile from t_user where username = ?";
        List<UserDto> userDtoList = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));
        if (userDtoList != null && userDtoList.size() == 1) {
            return userDtoList.get(0);
        }
        return null;
    }
}

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

import com.geek.security.distributed.uaa.dao.UserDao;
import com.geek.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;

@Service
public class SpringDataUserDetailService implements UserDetailsService {

    @Autowired
    private UserDao userDao;

    // 根据账号查询用户信息。
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 将来连接数据库根据账号查询用户信息。
        UserDto userDto = userDao.getUserByUsername(username);
        if (userDto == null) {
            // 如果用户查不到,返回 null,由 provider 来抛出异常。
            return null;
        }

        // 根据用户 id 查询用户的权限。
        List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
        // 将 permissions 转为数组。
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);

        UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray).build();


        // 登录账号。
        System.out.println("username = " - username);
        // 暂时采用模拟方式。
//        UserDetails userDetails = User.withUsername("zhangsan").password("$2a$10$ZV2gne5gM44.eRTi0KFOK.MJ/OmjB3h6Aw1sUk9YoOU8rbSUHtYwG").authorities("p1").build();

//        UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities("p1").build();

        return userDetails;
    }
}

package com.geek.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;

/**
 * 安全配置。
 * 用户信息、密码编码器、安全拦截机制。
 */
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 认证管理器。
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    // 密码编码器。(密码比对方式)。
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 安全拦截机制。(关键)。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();
    }
}

最终配置~AuthorizationServer。

package com.geek.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.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    public AuthorizationServer() {
        super();
    }

    // 设置授权码模式的授权码如果存取。
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService);// 客户端详情服务。
        services.setSupportRefreshToken(true);// 支持刷新令牌。
        services.setTokenStore(tokenStore);// 令牌存储策略。
        services.setAccessTokenValiditySeconds(7200);// 令牌默认有效期 2 小时。
        services.setRefreshTokenValiditySeconds(259200);// 刷新令牌默认有效期 3 天。
        return services;
    }

    /**
     * - AuthorizationServerSecurityConfigurer security。
     * 配置令牌端点的安全约束。
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//        super.configure(security);
        security
                .tokenKeyAccess("permitAll()")// oauth/token_key 公开。
                .checkTokenAccess("permitAll()")// oauth/check_token 公开。
                .allowFormAuthenticationForClients();// 表单认证。(申请令牌)。
    }

    /**
     * - ClientDetailsServiceConfigurer。
     * 配置客户端详情服务(ClientDetailService)。客户端详情信息在这里初始化。
     * 客户端详情信息直接写死在这里或通过数据库存储调取详情信息。
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//        super.configure(clients);

        clients.inMemory()// 使用 in-memory 存储。
                .withClient("c1")// clientId
                .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥。
                .resourceIds("res1")
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该 client 允许的授权类型。
                .scopes("all")// 允许的授权范围。
                .autoApprove(false)// false 跳转到授权页面。
                // 验证回调地址。
                .redirectUris("http://www.baidu.com");
    }

    /**
     * - AuthorizationServerEndpointsConfigurer endpoints。
     * 用来配置令牌(token)的访问端点和令牌服务(token services)。
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//        super.configure(endpoints);
        endpoints
                .authenticationManager(authenticationManager)// 认证管理器。
                .authorizationCodeServices(authorizationCodeServices)// 授权码码服务。
                .tokenServices(tokenServices())// 令牌管理服务。
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }
}



授权模式。

授权码模式~code。

在这里插入图片描述

  • ① 资源拥有者打开客户端,客户端要求资源拥有者给予授权,ta 将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。

http://192.168.85.1:53020/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redireect_uri=https://www.baidu.com/

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

地址栏返回授权码

https://www.baidu.com/?code=mbzHcV

  • ② 浏览器出现向授权服务器授权页面,之后将用户同意授权。

  • ③ 授权服务器将授权码(AuthorizationCode)转经浏览器发送给 client(通过 redirect_url)。

  • ④ 客户端拿着授权码向授权服务器索要访问 access_token。

{“error”:“method_not_allowed”,“error_description”:“Request method ‘GET’ not supported”}

POST 请求。

192.168.85.1:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=mbzHcV&redirect=http://www.baidu.com

  • client_id。
    客户端准入标识。
  • client_secret。
    客户端秘钥。
  • grant_type。
    授权类型。authorization_code,授权码模式。
  • code。
    授权码。就是刚刚获取的授权码。
    注意:授权码使用一次就无效了。需要重新申请。
  • redirect_uri。
    申请授权码时的跳转 uri。一定要和申请授权码时用的 redirect_uri 一致。
  • ⑤ 授权服务器返回令牌(access_token)。
{
    "access_token": "c4a9046c-100e-45b9-9b49-d20eba5b6c24",
    "token_type": "bearer",
    "refresh_token": "4dc66670-8505-4d3b-b30f-d26fcd8dff3d",
    "expires_in": 7199,
    "scope": "all"
}

一个授权码只能使用一次。

{
    "error": "invalid_grant",
    "error_description": "Invalid authorization code: mbzHcV"
}

四种模式中最安全的。一般用于 client 是 web 服务器端应用或第三方原生 app 调用资源服务。因为在这种模式中 access_token 不会经过浏览器或移动端的 app,而是直接从服务端去交换,这样就最大限度的减小了令牌泄露的风险。



简化模式~token。

在这里插入图片描述

  • ① 资源拥有者打开客户端,客户端要求资源拥有者给予授权,ta 将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。

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

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

地址栏返回access_token

https://www.baidu.com/#access_token=c4a9046c-100e-45b9-9b49-d20eba5b6c24&token_type=bearer&expires_in=6553

  • ② 浏览器出现向授权服务器授权页面,之后将用户同意授权。

  • ③ 授权服务器将令牌(access_token)以 Hash 的形式存放在重定向 uri 的 fragment 中发送给浏览器。

注:fragment 主要是用来标识 uri 所标识资源里的某个资源,在 uri 的末尾通过(#)作为 fragment 的开头,其中 # 不属于 fragment 值。eg. https://domain/index#L18。这个 URI 中的 L18 就是 fragment 的值。js 通过响应浏览器地址栏变化的方式能获取到 fragment。

确认授权后,浏览器会重定向到指定路径(oauth_client_details 表中的 web_server_redirect_uri)并以 Hash 的形式存放在重定向 uri 的 fragment 中。



密码模式。

在这里插入图片描述

  • ① 资源拥有者将用户名、密码发送给客户端。

  • ② 客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token)。

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

{“error”:“method_not_allowed”,“error_description”:“Request method ‘GET’ not supported”}

POST!

  • client_id。
    客户端准入标识。
  • client_secret。
    客户端秘钥
  • grant_type。
    授权类型。password 标识密码模式。
  • username。
    资源拥有者用户名。
  • password。
    资源拥有者密码。
{
    "access_token": "c4a9046c-100e-45b9-9b49-d20eba5b6c24",
    "token_type": "bearer",
    "refresh_token": "4dc66670-8505-4d3b-b30f-d26fcd8dff3d",
    "expires_in": 5829,
    "scope": "all"
}

这种模式十分简单,但是缺意味着直接将用户敏感信息泄露给了 client。因此这种模式只能用于 client 是我们自己开发的情况下,第一方原生 app 或第一方单页面应用。



客户端模式。

在这里插入图片描述

  • 客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)。
  • 确认客户端身份无误后,将令牌(access_token)发送给 client。

http://192.168.0.108:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials&redirect_uri=https://www.baidu.com

  • client_id。
    客户端准入标识。
  • client_secret。
    客户端秘钥。
  • grant_type。
    授权类型。client_credentials 表示客户端模式。

{“error”:“method_not_allowed”,“error_description”:“Request method ‘GET’ not supported”}

{
    "access_token": "6460ea03-3d23-4ad8-adef-dd3a225f16cd",
    "token_type": "bearer",
    "expires_in": 7141,
    "scope": "all"
}


资源服务测试。

distributed-security-order 模块中。

package com.geek.security.ditributed.order.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.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;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "res1";


    public ResourceServerConfig() {
        super();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//        super.configure(resources);
        resources
                .resourceId(RESOURCE_ID)// 资源 id。
                .tokenServices(tokenService())// 验证令牌的服务。
                .stateless(true);
    }

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

    @Override
    public void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.
                authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('all')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 基于 token,不再记录 session。
    }
}

  • controller。
package com.geek.security.ditributed.order.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @GetMapping("/r1")
    @PreAuthorize("hasAnyAuthority('p1')")
    public String r1() {
        return "访问资源 1。";
    }
}

http://192.168.85.1:53021/order/r1

{
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource"
}
  • 使用。

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

{
    "access_token": "9288481b-a6cb-427b-ac67-fb5354e2201c",
    "token_type": "bearer",
    "refresh_token": "db59cd02-5ac8-4815-9f2a-6c3587419578",
    "expires_in": 7125,
    "scope": "all"
}

post 请求。

http://192.168.85.1:53020/uaa/oauth/check_token
token=9288481b-a6cb-427b-ac67-fb5354e2201c

{
    "aud": [
        "res1"
    ],
    "exp": 1589051315,
    "user_name": "zhangsan",
    "authorities": [
        "p1",
        "p3"
    ],
    "client_id": "c1",
    "scope": [
        "all"
    ]
}

在这里插入图片描述



添加安全访问控制。
package com.geek.security.ditributed.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;

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 安全拦截机制。(最重要)。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //        super.configure(http);
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()
                .anyRequest().permitAll();
    }
}

{
    "error": "access_denied",
    "error_description": "不允许访问"
}


JWT。

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

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

  • JWT。

JSON Web Token(JWT)是一种紧凑的、URL 安全的方法,用于表示双方之间要传输的声明。JWT 中的声明被编码为 JSON 对象,该对象用作 JSON Web 签名(JWS)结构的有效负载或 JSON Web 加密(JWE)结构的明文,从而使声明能够通过消息验证码(MAC)进行数字签名或完整性保护和/或加密。
JWT 是一个开放的行业标准(RFC 7519),ta 定义了一种简洁的、自包含的协议格式,用于在通信双方传递 json 对象,传递的信息经过数字签名可以被验证和信任。JWT 可以使用 HMAC 算法或 RSA 的公钥/私钥对来签名,防止被篡改。

https://jwt.io/

https://tools.ietf.org/html/rfc7519

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


JWT 令牌结构。

三部分,以 . 分隔。

xxxxx.yyyyy.zzzzz。

Header。

头部分包括令牌的类型(即 JWT)及使用的哈希算法(如 HMAC SHA256 或 RSA)。

eg. 将以下内容使用 base64url 编码,得到的字符串就是 JWT 令牌的第一部分。

{
	"alg": "hs256",
	"typ": "JWT"
}
Payload。

第二部分是负载,内容也是一个 json 对象,ta 是存放有效信息的地方,ta 可以存放 jwt 提供的现成字段,eg. iss(签发者),exp(过期时间戳),sub(面向的用户)等,也可以自定义字段。

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容分。

eg. 将以下内容使用 base64url 编码,得到的字符串就是 JWT 令牌的第二部分。

{
	"sub": "1234567890",
	"name": "456",
	"admin": true
}
signature。

第三部分是签名,此部分用于防止 jwt 内容被篡改。

这个比分使用 base64url 将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用 header 中声明的签名算法进行签名。

eg.

HMACSHA256(
	base64UrlEncode(header) - "." +
	base64UrlEncode(payload),
	secret)


配置 jwt 令牌服务(uaa 中)。
package com.geek.security.ditributed.order.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;

@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
        // jwt 令牌存储方案。
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);// 对称秘钥,资源服务器使用该秘钥来验证。
        return jwtAccessTokenConverter;
    }

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

package com.geek.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.InMemoryAuthorizationCodeServices;
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 java.util.Arrays;

@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 jwtAccessTokenConverter;
    // ~~~

    public AuthorizationServer() {
        super();
    }

    // 设置授权码模式的授权码如果存取。
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService);// 客户端详情服务。
        services.setSupportRefreshToken(true);// 支持刷新令牌。
        services.setTokenStore(tokenStore);// 令牌存储策略。

        // ~~~令牌增强。
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);
        // ~~~

        services.setAccessTokenValiditySeconds(7200);// 令牌默认有效期 2 小时。
        services.setRefreshTokenValiditySeconds(259200);// 刷新令牌默认有效期 3 天。
        return services;
    }

    /**
     * - AuthorizationServerSecurityConfigurer security。
     * 配置令牌端点的安全约束。
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//        super.configure(security);
        security
                .tokenKeyAccess("permitAll()")// oauth/token_key 公开。
                .checkTokenAccess("permitAll()")// oauth/check_token 公开。
                .allowFormAuthenticationForClients();// 表单认证。(申请令牌)。
    }

    /**
     * - ClientDetailsServiceConfigurer。
     * 配置客户端详情服务(ClientDetailService)。客户端详情信息在这里初始化。
     * 客户端详情信息直接写死在这里或通过数据库存储调取详情信息。
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//        super.configure(clients);

        clients.inMemory()// 使用 in-memory 存储。
                .withClient("c1")// clientId
                .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥。
                .resourceIds("res1")
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该 client 允许的授权类型。
                .scopes("all")// 允许的授权范围。
                .autoApprove(false)// false 跳转到授权页面。
                // 验证回调地址。
                .redirectUris("http://www.baidu.com");
    }

    /**
     * - AuthorizationServerEndpointsConfigurer endpoints。
     * 用来配置令牌(token)的访问端点和令牌服务(token services)。
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//        super.configure(endpoints);
        endpoints
                .authenticationManager(authenticationManager)// 认证管理器。
                .authorizationCodeServices(authorizationCodeServices)// 授权码码服务。
                .tokenServices(tokenServices())// 令牌管理服务。
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }
}



测试。

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

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE1ODkxMzc2MjQsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiN2MxMGY5OTYtYzc3OC00NTNiLWI2N2ItYWUyMGQ0Y2Y2OWE5IiwiY2xpZW50X2lkIjoiYzEifQ.5kupjk5Nywhz7H1SA01O4f2xP_iGjmuaeTHv4yC7_lg",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI3YzEwZjk5Ni1jNzc4LTQ1M2ItYjY3Yi1hZTIwZDRjZjY5YTkiLCJleHAiOjE1ODkzODk2MjQsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNzI4NDI2ODMtMGEyNi00N2FiLTg4OTAtZjc5MDQ0Y2YzODFhIiwiY2xpZW50X2lkIjoiYzEifQ.efbbhALxsvOXZONWIvAaOb9KG3SVd7qeze-uJrvEpsg",
    "expires_in": 7199,
    "scope": "all",
    "jti": "7c10f996-c778-453b-b67b-ae20d4cf69a9"
}

post。

http://192.168.85.1:53020/uaa/oauth/check_token
~ token: ~
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE1ODkxMzc2MjQsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiN2MxMGY5OTYtYzc3OC00NTNiLWI2N2ItYWUyMGQ0Y2Y2OWE5IiwiY2xpZW50X2lkIjoiYzEifQ.5kupjk5Nywhz7H1SA01O4f2xP_iGjmuaeTHv4yC7_lg

http://192.168.85.1:53021/order/r1



校验 jwt 令牌。

资源服务需要和授权服务拥有一致的签字、令牌服务等。

  • 将授权服务中的 TokenConfig 类拷贝到资源服务中。
package com.geek.security.ditributed.order.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;

@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
        // jwt 令牌存储方案。
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);// 对称秘钥,资源服务器使用该秘钥来验证。
        return jwtAccessTokenConverter;
    }

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

  • 屏蔽资源服务原来的令牌服务类。
package com.geek.security.ditributed.order.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.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.TokenStore;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "res1";

    @Autowired
    private TokenStore tokenStore;

    public ResourceServerConfig() {
        super();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//        super.configure(resources);
        resources
                .resourceId(RESOURCE_ID)// 资源 id。
//                .tokenServices(tokenService())// 验证令牌的服务。

                // ~~~
                .tokenStore(tokenStore)
                // ~~~

                .stateless(true);
    }

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

    @Override
    public void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.
                authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('all')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 基于 token,不再记录 session。
    }
}

post。

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

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE1ODkxMzg0NjEsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiMDAzZDEwZjItMTNmNi00NDljLTlmNWYtYjI3OGI4ZTM4NzBmIiwiY2xpZW50X2lkIjoiYzEifQ.OByjXUdYp1J3jroFP08UZ2SSavCDUw4mOQHiZDoSuUM",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiIwMDNkMTBmMi0xM2Y2LTQ0OWMtOWY1Zi1iMjc4YjhlMzg3MGYiLCJleHAiOjE1ODkzOTA0NjEsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiMjFiZTA5YWMtYjg3ZS00NWY1LTk1MzktOTA3NjQ3MGQ0YzhhIiwiY2xpZW50X2lkIjoiYzEifQ.ufSMn_cVnDiGMO8DOC1bHNGu8mAfCSHK1NO1935wWWg",
    "expires_in": 7199,
    "scope": "all",
    "jti": "003d10f2-13f6-449c-9f5f-b278b8e3870f"
}

GET

http://192.168.85.1:53021/order/r1

Authorization
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE1ODkxMzg0NjEsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiMDAzZDEwZjItMTNmNi00NDljLTlmNWYtYjI3OGI4ZTM4NzBmIiwiY2xpZW50X2lkIjoiYzEifQ.OByjXUdYp1J3jroFP08UZ2SSavCDUw4mOQHiZDoSuUM

访问资源 1。



OAuth 2.0 完善配置环境。

CREATE TABLE `user_db`.`oauth_client_details` (
    `client_id` VARCHAR(255) NOT NULL COMMENT '客户端标识。',
    `resource_ids` VARCHAR(255) NULL DEFAULT NULL COMMENT '接入资源列表。',
    `client_secret` VARCHAR(255) NULL DEFAULT NULL COMMENT '客户端密钥。',
    `scope` VARCHAR(255) NULL DEFAULT NULL,
    `authorized_grant_types` VARCHAR(255) NULL DEFAULT NULL,
    `web_server_redirect_uri` VARCHAR(255) NULL DEFAULT NULL,
    `authorities` VARCHAR(255) NULL DEFAULT NULL,
    `access_token_validity` INT(11) NULL DEFAULT NULL,
    `refresh_token_validity` INT(11) NULL DEFAULT NULL,
    `additional_information` LONGTEXT NULL,
    `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP (0) on UPDATE CURRENT_TIMESTAMP(0),
    `archived` TINYINT(4) NULL DEFAULT NULL,
    `trusted` TINYINT(4) NULL DEFAULT NULL,
    `autoapprove` VARCHAR(255) NULL DEFAULT NULL,
    PRIMARY KEY (`client_id`) USING BTREE
)  ENGINE=INNODB DEFAULT CHARACTER SET=UTF8 COMMENT='接入客户端信息。' ROW_FORMAT=DYNAMIC;

INSERT INTO `user_db`.`oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `access_token_validity`, `refresh_token_validity`, `create_time`, `archived`, `trusted`, `autoapprove`) VALUES ('c1', 'res1', '$2a$10$FIGNhDpg/OrZFUTiuSQGiuir2ABr1vDBbpuK9ooTKgwl1y3HpSq1q', 'ROLE_ADMIN,ROLE_USER,ROLE_API', 'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com', '7200', '259200', '2020-05-11 12:36:28', '0', '0', 'false');
INSERT INTO `user_db`.`oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `access_token_validity`, `refresh_token_validity`, `create_time`, `archived`, `trusted`, `autoapprove`) VALUES ('c2', 'res2', 'secret', 'ROLE_API', 'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com', '31536000', '2592000', '2020-05-11 12:36:28', '0', '0', 'false');

CREATE TABLE `user_db`.`oauth_code` (
  `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `code` VARCHAR(255) CHARACTER SET 'utf8' COLLATE 'utf8_general_ci' NULL DEFAULT NULL,
  `authentication` BLOB NULL,
  INDEX `code_index` USING BTREE (`code`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
ROW_FORMAT = COMPACT;

package com.geek.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.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;

@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 jwtAccessTokenConverter;
    // ~~~
    @Autowired
    private PasswordEncoder passwordEncoder;

    public AuthorizationServer() {
        super();
    }

    // 设置授权码模式的授权码如果存取。
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    // 令牌管理服务。
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService);// 客户端详情服务。
        services.setSupportRefreshToken(true);// 支持刷新令牌。
        services.setTokenStore(tokenStore);// 令牌存储策略。

        // ~~~令牌增强。
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);
        // ~~~

        services.setAccessTokenValiditySeconds(7200);// 令牌默认有效期 2 小时。
        services.setRefreshTokenValiditySeconds(259200);// 刷新令牌默认有效期 3 天。
        return services;
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        return new JdbcAuthorizationCodeServices(dataSource);// 设置授权码模式的授权码如何存取。
    }

    /**
     * - AuthorizationServerSecurityConfigurer security。
     * 配置令牌端点的安全约束。
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//        super.configure(security);
        security
                .tokenKeyAccess("permitAll()")// oauth/token_key 公开。
                .checkTokenAccess("permitAll()")// oauth/check_token 公开。
                .allowFormAuthenticationForClients();// 表单认证。(申请令牌)。
    }

    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
        return jdbcClientDetailsService;
    }

    /**
     * - ClientDetailsServiceConfigurer。
     * 配置客户端详情服务(ClientDetailService)。客户端详情信息在这里初始化。
     * 客户端详情信息直接写死在这里或通过数据库存储调取详情信息。
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//        super.configure(clients);

        clients
                .withClientDetails(clientDetailsService);

//        clients.inMemory()// 使用 in-memory 存储。
//                .withClient("c1")// clientId
//                .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥。
//                .resourceIds("res1")
//                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该 client 允许的授权类型。
//                .scopes("all")// 允许的授权范围。
//                .autoApprove(false)// false 跳转到授权页面。
//                // 验证回调地址。
//                .redirectUris("http://www.baidu.com");
    }

    /**
     * - AuthorizationServerEndpointsConfigurer endpoints。
     * 用来配置令牌(token)的访问端点和令牌服务(token services)。
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//        super.configure(endpoints);
        endpoints
                .authenticationManager(authenticationManager)// 认证管理器。
                .authorizationCodeServices(authorizationCodeServices)// 授权码码服务。
                .tokenServices(tokenServices())// 令牌管理服务。
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }
}

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

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJleHAiOjE1ODkxODA1MjQsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiYWI4MzhiM2UtMmI0ZC00MTU4LTlkMzItMmNhMjZjNjkxMDg1IiwiY2xpZW50X2lkIjoiYzEifQ.J-F97Fytb5NTinQXbXYOEHsXirVFK7xdKlpKbJsNR9w",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJhdGkiOiJhYjgzOGIzZS0yYjRkLTQxNTgtOWQzMi0yY2EyNmM2OTEwODUiLCJleHAiOjE1ODk0MzI1MjQsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNDUyMTRlODgtODRjZS00YWVhLTkyZjgtZTk5MjFhMTAwNTlmIiwiY2xpZW50X2lkIjoiYzEifQ.xUp3tVOut6RWTdb61xccIC0mA6hU1Yho5rHIIVMrjKA",
    "expires_in": 7199,
    "scope": "ROLE_ADMIN ROLE_USER ROLE_API",
    "jti": "ab838b3e-2b4d-4158-9d32-2ca26c691085"
}
  • 校验令牌合法性。POST。

http://192.168.85.1:53020/uaa/oauth/check_token

token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJleHAiOjE1ODkxODA1MjQsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiYWI4MzhiM2UtMmI0ZC00MTU4LTlkMzItMmNhMjZjNjkxMDg1IiwiY2xpZW50X2lkIjoiYzEifQ.J-F97Fytb5NTinQXbXYOEHsXirVFK7xdKlpKbJsNR9w

{
    "aud": [
        "res1"
    ],
    "user_name": "zhangsan",
    "scope": [
        "ROLE_ADMIN",
        "ROLE_USER",
        "ROLE_API"
    ],
    "exp": 1589180524,
    "authorities": [
        "p1",
        "p3"
    ],
    "jti": "ab838b3e-2b4d-4158-9d32-2ca26c691085",
    "client_id": "c1"
}

http://192.168.85.1:53020/uaa/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_ADMIN&redireect_uri=https://www.baidu.com/

https://www.baidu.com/?code=3yHpyI

授权码存入了数据库。

create_time, code, authentication
'2020-05-01 02:30:18', '3yHpyI', ?



注册中心。

distributed-security-discovery

spring:
  application:
    name: distributed-discovery
server:
  port: 53000
eureka:
  server:
    enable-self-preservation: false # 关闭服务器自我保护。客户端心跳检测 15 分钟内错误达到 80% 服务器会保护,导致别人认为还是好用的服务。
    eviction-interval-timer-in-ms: 10000 # 清理间隔(单位毫秒,默认是 60 * 1000)。5 秒将客户端剔除的服务在服务注册列表中剔除。
    shouldUseReadOnlyResponseCache: true # Eureka 是 CAP 理论中基于 AP 策略,为了保证强一致性关闭此切换 CA。默认不关闭。false 关闭。
  client:
    register-with-eureka: false # 不作为一个客户端注册到注册中心。
    fetch-registry: false # 为 true时可以 启动,但报异常:can't execute request on any known server。
    initial-instance-info-replication-interval-seconds: 10
    service-url:
      defaulZone: 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.geek.security.distributed.discovery;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServer_53000Application {
    public static void main(String[] args) {
        SpringApplication.run(DiscoveryServer_53000Application.class, args);
    }
}



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

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.serverUrl.defaultZone:http://localhost:53000/eureka/
eureka.instance.prefer-ip-address=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
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
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.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.142.143:3307/user_db
spring.datasource.username=root
spring.datasource.password=root
#
eureka.client.serverUrl.defaultZone: http://localhost:53000/eureka
eureka.instance.prefer-ip-address=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



网关。

网关整合 OAuth 2.0 两种思路。

  • 认证服务器生成 jwt 令牌,所有请求统一在网层关验证,判断权限等操作。

  • 由各资源服务处理,网关只做请求转发。

我们选择第一种,API 网关作为 OAuth 2.0 的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息(JsonToken)给微服务。这样下游微服务就不需要关心令牌格式解析以及 OAuth 2.0 相关机制了。

API 网关。

  • 作为 OAuth 2.0 的资源服务器角色,实现接入方权限拦截。
  • 令牌解析并转发当前登录用户信息(明文 token)给微服务。

微服务拿到明文 token (明文 token 中包含登录用户的身份和权限信息)后也需要做两件事。

  • 用户授权拦截(看当前用户是否有权访问该资源)。
  • 将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)。
<?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.geek</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>distributed-security-gateway</artifactId>

    <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>

spring.application.name=gateway-server
#
server.port=53010
spring.main.allow-bean-definition-overriding=true
#
logging.level.root=info
logging.level.org.springframework.web=info
#
zuul.retryable=true
zuul.ignored-services=*
zuul.add-host-header=true
zuul.sensitive-headers=*
#
zuul.routes.uaa-service.strip-prefix=false
zuul.routes.uaa-service.path=/uaa/**
#
zuul.routes.order-service.strip-prefix=false
zuul.routes.order-service.path=/order/**
#
eureka.client.serverUrl.defaultZone=http://localhost:53000/eureka/
eureka.instance.prefer-ip-address=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

  • 统一认证服务(UAA)和统一用户服务都是网关下的微服务,需要在网关上新增路由配置。
zuul.routes.uaa-service.strip-prefix=false
zuul.routes.uaa-service.path=/uaa/**

zuul.routes.user-service.strip-prefix=false
zuul.routes.user-service.path=/order/**
  • 启动类。
package com.geek.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;

@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class GatewayServer_53010Application {
    public static void main(String[] args) {
        SpringApplication.run(GatewayServer_53010Application.class, args);
    }
}

  • 配置。
package com.geek.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;

@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
        // jwt 令牌存储方案。
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);// 对称秘钥,资源服务器使用该秘钥来验证。
        return jwtAccessTokenConverter;
    }

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

package com.geek.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
//@EnableResourceServer
public class ResourceServerConfig {

    private static final String RESOURCE_ID = "res1";

    public ResourceServerConfig() {
        super();
    }

    /**
     * 统一认证服务(UAA)资源拦截。
     */
    @Configuration
    @EnableResourceServer
    public class UAAServerConfig extends ResourceServerConfigurerAdapter {

        @Autowired
        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//        super.configure(resources);
            resources
                    .resourceId(RESOURCE_ID)// 资源 id。
//                .tokenServices(tokenService())// 验证令牌的服务。

                    .tokenStore(tokenStore)
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
            http
                    .authorizeRequests()
                    .antMatchers("/uaa/**").permitAll();
        }

    }

    /**
     * 统一用户服务。
     */
    @Configuration
    @EnableResourceServer
    public class OrderServerConfig extends ResourceServerConfigurerAdapter {

        @Autowired
        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//        super.configure(resources);
            resources
                    .resourceId(RESOURCE_ID)// 资源 id。
//                .tokenServices(tokenService())// 验证令牌的服务。

                    .tokenStore(tokenStore)
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
            http
                    .authorizeRequests()
                    .antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
        }

    }

    // 配置其他资源服务。

}

package com.geek.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;

@Configuration
//@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 安全拦截机制。(最重要)。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //        super.configure(http);
        http
                .authorizeRequests()
                .antMatchers("/r/**").permitAll()
                .and().csrf().disable();
    }
}



转发明文 token 给微服务。
  • 实现 Zuul 前置过滤器,完成当前登录用户信息提取,并放入转发微服务的 request 中。
utils。
package com.geek.security.distributed.gateway.common;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Base64;

public class EncryptUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(EncryptUtil.class);

    public static String encodeBase64(byte[] bytes) {
        return Base64.getEncoder().encodeToString(bytes);
    }

    public static String encodeUTF8StringBase64(String str) {
        String encoded = null;
        try {
            encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            LOGGER.warn("不支持的编码格式。", e);
        }
        return encoded;
    }

    public static String decodeUTF8StringBase64(String str) {
        String decoded = null;
        byte[] bytes = Base64.getDecoder().decode(str);
        try {
            decoded = new String(bytes, "utf-8");
        } catch (UnsupportedEncodingException e) {
//            e.printStackTrace();
            LOGGER.warn("不支持的编码格式。", e);
        }
        return decoded;
    }

    public static String encodeURL(String url) {
        String encoded = null;
        try {
            encoded = URLEncoder.encode(url, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            LOGGER.warn("URLEncode 失败。", e);
        }
        return encoded;
    }

    public static String decodeURL(String url) {
        String decoded = null;
        try {
            decoded = URLDecoder.decode(url, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            LOGGER.warn("URLDecode 失败。", e);
        }
        return decoded;
    }

    public static void main(String[] args) {
        String string = "abcd{'a':'b'}";
        System.out.println("string = " - string);
        String encoded = EncryptUtil.encodeUTF8StringBase64(string);
        System.out.println("encoded = " - encoded);
        String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
        System.out.println("decoded = " - decoded);

        String url = "== wo";
        System.out.println("url = " - url);
        String encodeURL = EncryptUtil.encodeURL(url);
        System.out.println("encodeURL = " - encodeURL);
        String decodeURL = EncryptUtil.decodeURL(encodeURL);
        System.out.println("decodeURL = " - decodeURL);
    }
}

/*
string = abcd{'a':'b'}
encoded = YWJjZHsnYSc6J2InfQ==
decoded = abcd{'a':'b'}
url = == wo
encodeURL = %3D%3D+wo
decodeURL = == wo

Process finished with exit code 0
 */
package com.geek.security.distributed.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.geek.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;

public class AuthFilter extends ZuulFilter {
    @Override
    public String filterType() {
//        return null;
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
        // 越小越优先。
    }

    @Override
    public boolean shouldFilter() {
//        return false;
        return true;
    }

    @Override
    public Object run() throws ZuulException {
//        return null;
        /**
         * 获取令牌内容。
         */
        RequestContext ctx = RequestContext.getCurrentContext();
        // 从安全上下文中拿到用户身份对象。
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (!(authentication instanceof OAuth2Authentication)) {
            // 无 token 访问网关内资源的情况,目前仅有 uaa 服务直接暴露。
            return null;
        }

        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
        Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
        // 用户身份。
        String principal = userAuthentication.getName();
//        Object principal = userAuthentication.getPrincipal();

        /**
         * 组装明文 token,转发给微服务,放入 header,名称为 json-token。
         */
        List<String> authorities = new ArrayList<>();
        // 从 userAuthentication 取出权限,放在 authorities 中。
        userAuthentication.getAuthorities().stream().forEach(a -> authorities.add(((GrantedAuthority) a).getAuthority()));

        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String, Object> jsonToken = new HashMap<>(requestParameters);

        // 把身份信息和权限信息放在 json 中,加入 http 的 header 中。
        if (userAuthentication != null) {
            // 获取当前用户的身份信息。
            jsonToken.put("principal", principal);
            // 获取当前用户的权限信息。
            jsonToken.put("authorities", authorities);
        }

        // 把身份信息和权限信息放在 json 中,加入 http 的 header 中。
        // 转发给微服务。
        ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));

        return null;
    }
}

  • bean。
package com.geek.security.distributed.gateway.config;

import com.geek.security.distributed.gateway.filter.AuthFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class ZuulConfig {

    @Bean
    public AuthFilter preFilter() {
        return new AuthFilter();
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.setMaxAge(18000L);
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(corsFilter);
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

在这里插入图片描述

  • Order 中的 ResourceServerConfig。

解析 Token。

.antMatchers("/**").access("#oauth2.hasScope('all')")
改为
.antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")


    @Override
    public void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.
                authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 基于 token,不再记录 session。
    }
  • order 中的过滤器。
package com.geek.security.ditributed.order.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.geek.security.ditributed.order.common.EncryptUtil;
import com.geek.security.ditributed.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 {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 解析出头中的 token。
        String token = httpServletRequest.getHeader("json-token");
        if (token != null) {
            // 解析 token。
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            // 将 token 转成 json 对象。
            JSONObject jsonObject = JSON.parseObject(json);
            // 用户身份信息。
            UserDto userDto = new UserDto();
            String principal = jsonObject.getString("principal");
            userDto.setUsername(principal);
            // 用户权限。
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
            // 将用户信息和权限填充到用户身份 token 对象中。
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDto, null, AuthorityUtils.createAuthorityList(authorities));
            usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            // 将 authentication 保存到安全上下文。
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

package com.geek.security.ditributed.order.controller;

import com.geek.security.ditributed.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("/r1")
    @PreAuthorize("hasAnyAuthority('p1')")
    public String r1() {
        // 获取用户的身份信息。
        UserDto userDto = (UserDto) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return userDto.getUsername() - "访问资源 1。";
    }
}

  • 测试。

POST

192.168.0.101:53010/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJleHAiOjE1OTA2MDAyOTcsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNWNlNDc5MTYtZjU5NC00MTI2LWJlNTYtMTJmZTcxNjRkYzc3IiwiY2xpZW50X2lkIjoiYzEifQ.wCAg3WfovA8kDeWowxNl4AuSAHwqYrW2OrnrUjHefvk",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJhdGkiOiI1Y2U0NzkxNi1mNTk0LTQxMjYtYmU1Ni0xMmZlNzE2NGRjNzciLCJleHAiOjE1OTA4NTIyOTcsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiZGFlZTQ2OTQtZGE3MS00ZGY1LWE5ZTktOWQ4ZjUxYTVmODk0IiwiY2xpZW50X2lkIjoiYzEifQ.45eT3qR4BrfIey2qxispumjpsWg6OAK55GyXjUealxg",
    "expires_in": 7199,
    "scope": "ROLE_ADMIN ROLE_USER ROLE_API",
    "jti": "5ce47916-f594-4126-be56-12fe7164dc77"
}

POST

192.168.0.101:53020/uaa/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJleHAiOjE1OTA2MDAyOTcsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNWNlNDc5MTYtZjU5NC00MTI2LWJlNTYtMTJmZTcxNjRkYzc3IiwiY2xpZW50X2lkIjoiYzEifQ.wCAg3WfovA8kDeWowxNl4AuSAHwqYrW2OrnrUjHefvk

{
    "aud": [
        "res1"
    ],
    "user_name": "zhangsan",
    "scope": [
        "ROLE_ADMIN",
        "ROLE_USER",
        "ROLE_API"
    ],
    "exp": 1590600297,
    "authorities": [
        "p1",
        "p3"
    ],
    "jti": "5ce47916-f594-4126-be56-12fe7164dc77",
    "client_id": "c1"
}
  • 访问资源。

192.168.0.101:53010/order/r1?Authorization=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJleHAiOjE1OTA2MDAyOTcsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNWNlNDc5MTYtZjU5NC00MTI2LWJlNTYtMTJmZTcxNjRkYzc3IiwiY2xpZW50X2lkIjoiYzEifQ.wCAg3WfovA8kDeWowxNl4AuSAHwqYrW2OrnrUjHefvk



扩展用户信息。

JWT 令牌中用户信息仅定义了 username。

  • 扩展 UserDetailsl。

  • 扩展 username 的内容,eg. 存入 json 数据内容。

@Service
public class SpringDataUserDetailService implements UserDetailsService {

        // 将 UserDto 转为 json。
        String principal = JSON.toJSONString(userDto);
        UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionArray).build();
//        UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray).build();
public class TokenAuthenticationFilter extends OncePerRequestFilter {

            // 用户身份信息。
//            UserDto userDto = new UserDto();
//            String principal = jsonObject.getString("principal");
//            userDto.setUsername(principal);

            UserDto userDto = JSON.parseObject(jsonObject.getString("principal"), UserDto.class);
package com.geek.security.ditributed.order.controller;

import com.geek.security.ditributed.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("/r1")
    @PreAuthorize("hasAnyAuthority('p1')")
    public String r1() {
        // 获取用户的身份信息。
        UserDto userDto = (UserDto) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String fullname = userDto.getFullname();
        return fullname - "访问资源 1。";
//        return userDto.getUsername() - "访问资源 1。";
    }
}



完结撒花。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

lyfGeek

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

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

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

打赏作者

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

抵扣说明:

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

余额充值