原创

基于springSecurity的用户管理脚手架(UMS)

UMS (user manage scaffolding) 用户管理脚手架: github gitee

JDKMavenMySQLRedisSpringBootSpringSecuritySpringSociallicense

用户管理脚手架集成:用户密码登录、手机登录、支持所有 JustAuth 支持的第三方授权登录、验证码、基于 RBAC 的 uri 访问权限控制功能、签到等功能。
通过配置文件与实现 用户服务 与 短信发生服务 两个 API 接口就可以实现上述功能,实现快速开发,只需要专注于业务逻辑。

ums-arch


一、UMS 功能列表

  • 验证码(图片,短信, 滑块)校验功能。
  • 手机登录功能,登录后自动注册。
  • 支持所有 JustAuth 支持的第三方授权登录,登录后自动注册或绑定。
    • 支持定时刷新 accessToken, 支持分布式定时任务。
    • 支持第三方授权登录的用户信息表与 token 信息表的缓存功能。
    • 支持第三方绑定与解绑及查询接口(top.dcenter.ums.security.core.oauth.repository.UsersConnectionRepository).
  • 访问权限控制功能。
  • 简化 session、remember me、csrf 等配置。
  • 根据设置的响应方式(JSON 与 REDIRECT)返回 json 或 html 数据。
  • 签到功能。

模块功能

模块 功能
core 验证码/用户名密码登录/手机登录且自动注册/OAuth2 login by JustAuth/访问权限控制/签到/简化HttpSecurity(session、remember me、csrf 等)配置/session redis 缓存/可配置的响应方式(JSON 与 REDIRECT)返回 json 或 html 数据
demo basic-example/basic-detail-example/permission-example/quickStart/session-detail-example/social-simple-example/social-detail-example/validate-codi-example

demo 演示功能

demo 演示功能
basic-example core 模块基本功能: 最简单的配置
basic-detail-example core 模块基本功能详细的配置: 含anonymous/session简单配置/rememberMe/csrf/登录路由/签到, 不包含session详细配置/验证码/手机登录/权限.
permission-example core 模块: 基于 RBAC 的权限功能设置
quickStart 快速开始示例
justAuth-security-oauth2-example OAuth2 详细示例: 引用的依赖是分离于 core 模块的独立 OAuth2 模块 top.dcenter:justAuth-spring-security-starter:1.0.0, OAuth2 功能都一样.
session-detail-example core 模块: session 与 session 缓存详细配置
validate-code-example core 模块基本功能: 验证码(含自定义滑块验证码), 手机登录配置
quickStart-1.2.0 social 版本快速开始示例
过时:social-simple-example social 模块基本功能: 简单的配置(第三方登录自动注册默认打开)
过时:social-detail-example social 模块功能详细配置: 第三方授权登录注册功能, 统一回调地址路由配置, 第三方登录绑定配置, 第三方授权登录用户信息表自定义与 redis 缓存设置

更新日志


二、maven

<dependency>
    <groupId>top.dcenter</groupId>
    <artifactId>ums-core-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>

三、TODO List:

- 1. 准备基于 spring-security5.4 添加 JWT, OAuth2 authenticate server

四、快速开始

1. 添加依赖:

<dependency>
    <groupId>top.dcenter</groupId>
    <artifactId>ums-core-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>
<!-- 为了解决 ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.8.1</version>
</dependency>

2. config:

server:
  port: 9090

spring:
  profiles:
    active: dev
  # mysql
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/ums?useSSL=false&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

  # session 简单配置
  session:
    # session 存储模式设置, 要导入相应的 spring-session 类的依赖, 默认为 none, 分布式应用把 session 放入 redis 等中间件
    store-type: none
    # session 过期时间
    timeout: PT300s

  # thymeleaf
  thymeleaf:
    encoding: utf-8
    prefix: classpath:/templates/
    suffix: .htm
    servlet:
      content-type: text/html;charset=UTF-8


# ums core
ums:
  # ================ 第三方授权登录相关配置 ================
  oauth:
    # 是否支持第三方授权登录功能, 默认: true
    enabled: true
    # 第三方服务商: providerId
    github:
      # 根据是否有设置 clientId 来动态加载相应 JustAuth 的 AuthXxxRequest
      client-id: 4d4ee00e82f669f2ea8d
      client-secret: 953ddbe871a08d6924053531e89ecc01d87195a8
    gitee:
      client-id: dcc38c801ee88f43cfc1d5c52ec579751c12610c37b87428331bd6694056648e
      client-secret: e60a110a2f6e7c930c2d416f802bec6061e19bfa0ceb0df9f6b182b05d8f5a58
    # 第三方登录授权登录 url 前缀, 不包含 ServletContextPath,默认为 /auth2/authorization.
    auth-login-url-prefix: /auth2/authorization
    # 第三方登录回调处理 url 前缀 ,也就是 RedirectUrl 的前缀, 不包含 ServletContextPath,默认为 /auth2/login.
    redirect-url-prefix: /auth2/login
    # 第三方登录回调的域名, 例如:http://localhost 默认为 "http://127.0.0.1",
    # redirectUrl 直接由 {domain}/{servletContextPath}/{redirectUrlPrefix}/{providerId}(ums.oauth.[qq/gitee/weibo])组成
    domain: http://localhost:9090
    proxy:
      # 用于国内代理(HttpClient)超时, 默认 PT3S
      timeout: PT3S
      # 用于国外网站代理(HttpClient)超时, 默认 PT15S
      foreign-timeout: PT150S

  # ================ 密码登录, session, remember-me, csrf等配置 ================
  client:
    # 设置登录后返回格式(REDIRECT 与 JSON): 默认 JSON
    login-process-type: redirect
    # 登录页(必须自己实现)
    login-page: /login
    # 登录失败页(必须自己实现)
    failure-url: /login
    # 登录成功页(必须自己实现)
    success-url: /
    # 设置登出 url, 默认为 /logout
    logout-url: /logout
    # 设置登出后跳转的 url(必须自己实现), 默认为 /login
    logout-success-url: /login
    # 不需要认证的静态资源 urls, 例如: /resources/**, /static/**
    ignoring-urls:
      - /static/**
    # 不需要认证的 uri(可以带 HttpMethod 后缀; 用:隔开), 例如: /user/** 或 /user/**:post, 默认为 空 Set.
    permit-urls:
      - /hello:GET
      - /login
    # 设置登录时用户名的 request 参数名称, 默认为 username
    usernameParameter: username
    # 设置登录时用户密码的 request 参数名称, 默认为 password
    passwordParameter: password

  # ================ 验证码配置 ================
  # 同一个 uri 由多种验证码同时配置, **优先级**如下:
  #  `SMS > CUSTOMIZE > SELECTION > TRACK > SLIDER > IMAGE`
  codes:
    # 图片验证码
    image:
      # 设置需要图片验证码认证的 uri(必须是非 GET 请求),多个 uri 用 “-” 或 ","号分开支持通配符,如:/hello,/user/*;默认为 /authentication/form
      auth-urls:
        - /authentication/form
      request-param-image-code-name: imageCode
    # 短信验证码
    sms:
      # 设置需要短信验证码认证的 uri(必须是非 GET 请求),多个 uri 用 “,”号分开支持通配符,如:/hello,/user/*;默认为 /authentication/form
      auth-urls:
        - /authentication/mobile
      request-param-mobile-name: mobile
      request-param-sms-code-name: smsCode

  # ================ 手机登录配置 ================
  mobile:
    login:
      # 手机验证码登录是否开启, 默认 false,
      # 手机验证码登录开启后 必须配置 ums.codes.sms.auth-urls=/authentication/mobile
      sms-code-login-is-open: true
      # 手机验证码登录请求处理url, 默认 /authentication/mobile
      login-processing-url-mobile: /authentication/mobile

  # ================ 签到配置 ================
  sign:
    # redis key(String) 转 byte[] 转换时所用的 charset, 默认: StandardCharsets.UTF_8
    charset: UTF_8
    # 用于 redis 签到 key 前缀,默认为: u:sign:
    sign-key-prefix: 'u:sign:'
    # 用于 redis 总签到 key 前缀,默认为: total:sign:
    total-sign-key-prefix: 'total:sign:'
    # 获取最近几天的签到情况, 不能大于 28 天, 默认为 7 天
    last-few-days: 7
    # 用户签到 redis key TTL, 默认: 二个月 , 单位: 秒
    total-expired: 5356800
    # 用户签到统计 redis key TTL, 默认: 二个月 , 单位: 秒
    user-expired: 2678400

---
spring:
  profiles: dev
  mvc:
    throw-exception-if-no-handler-found: true
  thymeleaf:
    cache: false

#debug: true

server:
  port: 9090
  servlet:
    context-path: /demo

3. 实现 UmsUserDetailsService 接口等:

UserDetailsService.java

package top.dcenter.ums.security.core.demo.service;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.ServletWebRequest;
import top.dcenter.ums.security.common.enums.ErrorCodeEnum;
import top.dcenter.ums.security.core.api.service.UmsUserDetailsService;
import top.dcenter.ums.security.core.exception.RegisterUserFailureException;
import top.dcenter.ums.security.core.exception.UserNotExistException;

import java.util.List;

/**
 *  用户密码与手机短信登录与注册服务:<br><br>
 *  1. 用于第三方登录与手机短信登录逻辑。<br><br>
 *  2. 用于用户密码登录逻辑。<br><br>
 *  3. 用户注册逻辑。<br><br>
 * @author YongWu zheng
 * @version V1.0  Created by 2020/9/20 11:06
 */
@Service
@Slf4j
public class UserDetailsServiceImpl implements UmsUserDetailsService {

    /**
     * 用户名
     */
    public static final String PARAM_USERNAME = "username";

    /**
     * 密码
     */
    public static final String PARAM_PASSWORD = "password";

    private final ObjectMapper objectMapper;

    private final JdbcTemplate jdbcTemplate;

    @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
    @Autowired(required = false)
    private UserCache userCache;
    /**
     * 用于密码加解密
     */
    @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
    @Autowired
    private PasswordEncoder passwordEncoder;

    public UserDetailsServiceImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    @SuppressWarnings("AlibabaUndefineMagicConstant")
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        try
        {
            // 从缓存中查询用户信息:
            // 从缓存中查询用户信息
            if (this.userCache != null)
            {
                UserDetails userDetails = this.userCache.getUserFromCache(username);
                if (userDetails != null)
                {
                    return userDetails;
                }
            }
            // 根据用户名获取用户信息

            // 获取用户信息逻辑。。。
            // ...

            // 示例:只是从用户登录日志表中提取的信息,
            log.info("Demo ======>: 登录用户名:{}, 登录成功", username);
            return new User(username,
                            passwordEncoder.encode("admin"),
                            true,
                            true,
                            true,
                            true,
                            AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_VISIT, ROLE_USER"));

        }
        catch (Exception e)
        {
            String msg = String.format("Demo ======>: 登录用户名:%s, 登录失败: %s", username, e.getMessage());
            log.error(msg, e);
            throw new UserNotExistException(ErrorCodeEnum.QUERY_USER_INFO_ERROR, e, username);
        }
    }


    @Override
    public UserDetails registerUser(String mobile) throws RegisterUserFailureException {

        if (mobile == null)
        {
            throw new RegisterUserFailureException(ErrorCodeEnum.MOBILE_NOT_EMPTY, null);
        }

        // 用户信息持久化逻辑。。。
        // ...

        log.info("Demo ======>: 手机短信登录用户 {}:注册成功", mobile);

        User user = new User(mobile,
                             passwordEncoder.encode("admin"),
                             true,
                             true,
                             true,
                             true,
                             AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_VISIT, ROLE_USER")
        );

        // 把用户信息存入缓存
        if (userCache != null)
        {
            userCache.putUserInCache(user);
        }

        return user;
    }

    @Override
    public UserDetails registerUser(ServletWebRequest request) throws RegisterUserFailureException {

        String username = getValueOfRequest(request, PARAM_USERNAME, ErrorCodeEnum.USERNAME_NOT_EMPTY);
        String password = getValueOfRequest(request, PARAM_PASSWORD, ErrorCodeEnum.PASSWORD_NOT_EMPTY);
        // ...

        // UserInfo userInfo = getUserInfo(request)

        // 用户信息持久化逻辑。。。
        // ...

        String encodedPassword = passwordEncoder.encode(password);

        log.info("Demo ======>: 用户名:{}, 注册成功", username);
        User user = new User(username,
                             encodedPassword,
                             true,
                             true,
                             true,
                             true,
                             AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_VISIT, ROLE_USER")
        );

        // 把用户信息存入缓存
        if (userCache != null)
        {
            userCache.putUserInCache(user);
        }

        return user;

    }

    @Override
    public UserDetails registerUser(AuthUser authUser, String username, String defaultAuthority) throws RegisterUserFailureException {

        // 第三方授权登录不需要密码, 这里随便设置的, 生成环境按自己的逻辑
        String encodedPassword = passwordEncoder.encode(authUser.getUuid());

        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(defaultAuthority);

        // ... 用户注册逻辑

        log.info("Demo ======>: 用户名:{}, 注册成功", username);

        // @formatter:off
        UserDetails user = User.builder()
                               .username(username)
                               .password(encodedPassword)
                               .disabled(false)
                               .accountExpired(false)
                               .accountLocked(false)
                               .credentialsExpired(false)
                               .authorities(grantedAuthorities)
                               .build();
        // @formatter:off

        // 把用户信息存入缓存
        if (userCache != null)
        {
            userCache.putUserInCache(user);
        }

        return user;
    }

    @Override
    public UserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        UserDetails userDetails = loadUserByUsername(userId);
        User.withUserDetails(userDetails);
        return User.withUserDetails(userDetails).build();
    }

    @Override
    public List<Boolean> existedByUserIds(String... userIds) throws UsernameNotFoundException {
        // ... 在本地账户上查询 userIds 是否已被使用
        return List.of(true, false, false);
    }

    private String getValueOfRequest(ServletWebRequest request, String paramName, ErrorCodeEnum usernameNotEmpty) throws RegisterUserFailureException {
        String result = request.getParameter(paramName);
        if (result == null)
        {
            throw new RegisterUserFailureException(usernameNotEmpty, request.getSessionId());
        }
        return result;
    }
}

UserController.java

package top.dcenter.ums.security.core.demo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import top.dcenter.ums.security.core.permission.config.EnableUriAuthorize;

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

/**
 *
 * @author YongWu zheng
 * @version V1.0  Created by 2020/9/20 20:04
 */
@Controller
@Slf4j
@EnableUriAuthorize()
public class UserController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @GetMapping("/")
    public String index(@AuthenticationPrincipal UserDetails userDetails, Model model) {
        if (userDetails != null)
        {
            model.addAttribute("username", userDetails.getUsername());
            model.addAttribute("roles", userDetails.getAuthorities());
        }
        else
        {
            model.addAttribute("username", "anonymous");
            model.addAttribute("roles", "ROLE_VISIT");
        }
        return "index";
    }

    @GetMapping("/me")
    @ResponseBody
    public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails, Authentication authentication) {

        Map<String, Object> map = new HashMap<>(16);
        map.put("authenticationHolder", SecurityContextHolder.getContext().getAuthentication());
        map.put("userDetails", userDetails);
        map.put("authentication", authentication);
        return map;
    }

}

4. 前端页面 :

login.htm: 放在 classpath:/templates/

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@1.11.1/dist/jquery.min.js"></script>

</head>
<body>
<h2>登录页面</h2>
<h3>表单登录</h3>
<h5>如果短信验证码与图片验证码同时配置时,优先使用短信验证码,图片验证码失效</h5>
<!-- 通过 th:action 的方式支持 csrf 或者 添加隐藏域<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> -->
<form id="reg-form" th:action="@{/authentication/form}" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username" value="admin" ><p style="color: #ff0000"
                                                                     id="error-name"></p></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password" value="admin"></td>
        </tr>
        <tr>
            <td>图形验证码:</td>
            <td>
                <input type="text" name="imageCode">
                <img class="img" th:src="@{/code/image}" style="width: 67px; height: 23px">
            </td>
        </tr>
        <tr>
            <td ><input type="checkbox" name="rememberMe" checked="true">记住我</input></td>
            <td><p style="color: #ff0000" id="error-code"></p></td>
        </tr>
        <tr>
            <td ><button id="btn-reg" type="button">登录ajax</button></td>
            <!-- 通过 form submit 不能接收错误信息, 通过 ajax 可接收错误信息 -->
            <td ><button type="submit">登录</button></td>
        </tr>
    </table>
</form>
<h3>手机登录</h3>
<form id="mobile-form" th:action="@{/authentication/mobile}" method="post">
    <table>
        <tr>
            <td>手机号码:</td>
            <td>
                <input type="tel" name="mobile" value="13345678980"><p style="color: #ff0000"
                                                                       id="error-name-mobile"></p>
                <a th:href="@{/code/sms?mobile=13345678980}" >发送验证码</a>
            </td>
        </tr>
        <tr>
            <td>手机验证码:</td>
            <td>
                <input type="text" name="smsCode">
            </td>
        </tr>
        <tr>
            <td ><input type="checkbox" name="rememberMe" checked="true">记住我</input></td>
            <td><p style="color: #ff0000" id="error-code-mobile"></p></td>
        </tr>
        <tr>
            <td ><button id="btn-mobile" type="button">登录ajax</button></td>
            <!-- 通过 form submit 不能接收错误信息, 通过 ajax 可接收错误信息 -->
            <td ><button type="submit">登录</button></td>
        </tr>
    </table>
</form>
<br><br>
<h3>社交登录</h3>
<a th:href="@{/auth2/authorization/gitee}">gitee登录</a>
<a th:href="@{/auth2/authorization/github}">github登录</a>
<a th:href="@{/auth2/authorization/gitee}">github登录</a>

<dev id="basePath" th:basePath="@{/}" style="display: none"/>
</body>
<script>
    var basePath = $("#basePath").attr("basePath");
    $.fn.serializeObject = function()
    {
        let o = {};
        let a = this.serializeArray();
        $.each(a, function() {
            if (o[this.name]) {
                if (!o[this.name].push) {
                    o[this.name] = [o[this.name]];
                }
                o[this.name].push(this.value || '');
            } else {
                o[this.name] = this.value || '';
            }
        });
        return o;
    }

    $(".img").click(function(){
        let uri = this.getAttribute("src");
        console.log(uri)
        let end = uri.indexOf('?', 0);
        console.log(end)
        if (end === -1) {
            uri = uri + '?'+ Math.random();
        } else {
            uri = uri.substring(0, end) + '?'+ Math.random();
        }
        console.log(uri)
        this.setAttribute('src', uri);
    });


    function submitFormByAjax(url, formId, errorNameId, errorCodeId, imgId, refresh) {
        return function () {
            console.log(JSON.stringify($(formId).serializeObject()))
            $.ajax({
                // 如果用的是模板,则 url 可以使用注入的方式,会跟着配置动态改变
                url: url,
                data: JSON.stringify($(formId).serializeObject()),
                type: "POST",
                dataType: "json",
                success: function (data) {
                    $(errorNameId).text("")
                    $(errorCodeId).text("")
                    console.log("==========注册成功============")
                    // 注册成功
                    // ...
                    console.log(data)
                    let uri = data.data.targetUrl
                    if (uri === null) {
                        uri = basePath
                    }
                    window.location.href = uri;
                },
                error: function (data) {
                    // 注册失败
                    $(errorNameId).text("")
                    $(errorCodeId).text("")
                    console.log("********注册失败*********")
                    console.log(data)
                    data = data.responseJSON
                    if (undefined !== data) {
                        console.log(data);
                        // 错误代码看ErrorCodeEnum
                        if (data.code >= 900 && data.code < 1000) {
                            $(errorNameId).text(data.msg)
                        } else if (data.code >= 600 && data.code < 700) {
                            $(errorCodeId).text(data.msg)
                        }
                    }
                    if (refresh) {
                        $(imgId).trigger("click");
                    }
                }
            })
            return
        };
    }


    $("#btn-mobile").click(
        submitFormByAjax($("#mobile-form").attr("action"), "#mobile-form", "#error-name-mobile", "#error-code-mobile", ".img-mobile", true)
    )


    $("#btn-reg").click(
        submitFormByAjax($("#reg-form").attr("action"), "#reg-form", "#error-name", "#error-code", ".img", true)
    )

</script>
</html>

index.htm

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
hello <span th:text="${username}">world!</span><br>
roles: <span th:text="${roles}"/>
    <!-- 通过 th:action 的方式支持 csrf 或者 添加隐藏域<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> -->
    <form th:action="@{/logout?logout}" method="post">
        <input type="submit" value="退出登录post"/>
    </form>
</body>
</html>

5. 访问前端页面

  • 浏览器访问 http://localhost:9090/demo/login, 至此集成了:登录校验,验证码、手机登录、第三方登录(JustAuth)、基于 RBAC 的 uri 访问权限控制功能, 签到等功能; 实现快速开发。
  • Quick Start 代码在 demo 模块 -> quickStart, 其他功能的详细配置说明参照: Configurations

五、接口使用说明:

实现对应功能时需要实现的接口:

  1. 用户服务: 必须实现
  2. 图片验证码: 如果不实现就会使用默认图片验证码, 实时产生验证码图片, 没有缓存功能
  3. 短信验证码: 默认空实现
  4. 滑块验证码: 如果不实现就会使用默认滑块验证码, 实时产生验证码图片, 没有缓存功能
  5. 自定义验证码:
  6. 访问权限控制功能: 基于 RBAC 的访问权限控制, 增加了更加细粒度的权限控制, 支持 restfulApi; 如: 对菜单与按钮的权限控制
    • AbstractUriAuthorizeService:
      • AbstractUriAuthorizeService 类中的方法getRolesAuthorities();
        getRolesAuthorities()返回值: Map<role, Map<uri, UriResourcesDTO>> 中UriResourcesDTO字段 uri
        permission 必须有值.
      • 默认实现了 hasPermission(..) 表达式, 实现 AbstractUriAuthorizeService 即生效,
        1. 默认启用 httpSecurity.authorizeRequests().anyRequest().access("hasPermission(request, authentication)"); 方式.
        2. 如果开启注解方式( @UriAuthorize 或 @EnableGlobalMethodSecurity(prePostEnabled = true) ): 则通过注解 @PerAuthority
          ("hasPermission('/users/', '/users/:list')") 方式生效.

六、Configurations:

功能 模块 demo模块--简单配置 demo模块--详细配置
1. 基本功能 core basic-example
2. 登录路由功能 core basic-detail-example
3. session core session-detail-example
4. remember-me core basic-detail-example
5. csrf core basic-detail-example
6. anonymous core basic-detail-example
7. 验证码 core validate-code-example
8. 手机登录 core basic-detail-example
9. 第三方登录 core basic-detail-example
10. 给第三方登录时用的数据库表 user_connection 与 auth_token 添加 redis cache core basic-detail-example
11. 签到 core basic-detail-example
12. 基于 RBAC 的访问权限控制功能 core permission-example
13. 线程池配置 core justAuth-security-oauth2-example

七、注意事项:

1. 基于 RBAC 的 uri 访问权限控制

  • 更新角色权限时必须调用 AbstractUriAuthorizeService#updateRolesAuthorities() 方法来刷新权限, 即可实时刷新角色权限.

    • 刷新权限有两种方式:一种发布事件,另一种是直接调用服务;推荐用发布事件(异步执行)。
      1. 推荐用发布事件(异步执行)
        applicationContext.publishEvent(new UpdateRolesAuthoritiesEvent(true));
      2. 直接调用服务
        abstractUriAuthorizeService.updateRolesAuthorities();

        2. HttpSecurity 配置问题:UMS 中的 HttpSecurityAware 配置与应用中的 HttpSecurity 配置冲突问题:

  • 如果是新建应用添加 HttpSecurity 配置, 通过下面的接口即可:

  • 如果是已存在的应用:
    • 添加 HttpSecurity 配置, 通过下面的接口即可: HttpSecurityAware
    • 已有的 HttpSecurity 配置, 让原有的 HttpSecurity 配置实现此接口进行配置: top.dcenter.security.core.api.config.HttpSecurityAware

3. 在 ServletContext 中存储的属性:

  • 属性名称: SecurityConstants.SERVLET_CONTEXT_AUTHORIZE_REQUESTS_MAP_KEY
  • 属性值: authorizeRequestMap>: key 为 PERMIT_ALL, DENY_ALL, ANONYMOUS, AUTHENTICATED
    , FULLY_AUTHENTICATED, REMEMBER_ME 的权限类型, value 为 UriHttpMethodTuple(uri不包含 servletContextPath)的 set.

4. servletContextPath 的值存储在 MvcUtil.servletContextPath :

  • 通过静态方法获取 MvcUtil.getServletContextPath()
  • MvcUtil.servletContextPath 的值是通过: SecurityAutoConfiguration#afterPropertiesSet() 接口注入

5. 验证码优先级:

  • 同一个 uri 由多种验证码同时配置, 优先级如下:
    SMS > CUSTOMIZE > SELECTION > TRACK > SLIDER > IMAGE

八、属性配置列表

属性配置列表
基本属性
签到属性
手机登录属性
验证码属性
第三方授权登录?sort_id=3006562)
线程池属性
第三方授权登录用户信息数据 redis 缓存配置
第三方授权登录用户信息表 user_connection sql 配置
过时:social_userConnection redisCache属性
过时:social属性

九、参与贡献

  1. Fork 本项目
  2. 新建 Feat_xxx 分支
  3. 提交代码
  4. 新建 Pull Request

十、流程图: 随着版本迭代会有出入

1. 滑块验证码(sliderValidateCode)

sliderValidateCode

七、时序图(Sequence Diagram)

crsf

crsf

getValidateCode

getValidateCode

ImageValidateCodeLogin

ImageValidateCodeLogin

logout

logout

OAuth2Banding

OAuth2Banding

OAuth2Login

OAuth2Login

OAuth2SignUp

OAuth2SignUp

rememberMe

rememberMe

scurityConfigurer

scurityConfigurer

securityRouter

securityRouter

session

session

SmsCodeLogin

SmsCodeLogin

uriAuthorize

uriAuthorize

正文到此结束
本文目录