UMS (user manage scaffolding) 用户管理脚手架: github gitee
用户管理脚手架集成:用户密码登录、手机登录、支持所有 JustAuth 支持的第三方授权登录、验证码、基于 RBAC 的 uri 访问权限控制功能、签到等功能。 通过配置文件与实现 用户服务 与 短信发生服务 两个 API 接口就可以实现上述功能,实现快速开发,只需要专注于业务逻辑。
一、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 数据demobasic-example/basic-detail-example/permission-example/quickStart/session-detail-example/social-simple-example/social-detail-example/validate-codi-example
demo 演示功能
demo演示功能
basic-examplecore 模块基本功能: 最简单的配置basic-detail-examplecore 模块基本功能详细的配置: 含anonymous/session简单配置/rememberMe/csrf/登录路由/签到, 不包含session详细配置/验证码/手机登录/权限.permission-examplecore 模块: 基于 RBAC 的权限功能设置quickStart快速开始示例justAuth-security-oauth2-exampleOAuth2 详细示例: 引用的依赖是分离于 core 模块的独立 OAuth2 模块 top.dcenter:justAuth-spring-security-starter:1.0.0, OAuth2 功能都一样.session-detail-examplecore 模块: session 与 session 缓存详细配置validate-code-examplecore 模块基本功能: 验证码(含自定义滑块验证码), 手机登录配置quickStart-1.2.0social 版本快速开始示例过时:social-simple-examplesocial 模块基本功能: 简单的配置(第三方登录自动注册默认打开)过时:social-detail-examplesocial 模块功能详细配置: 第三方授权登录注册功能, 统一回调地址路由配置, 第三方登录绑定配置, 第三方授权登录用户信息表自定义与 redis 缓存设置
更新日志
二、maven:
<dependency>
<groupId>top.dcenter
</groupId>
<artifactId>ums-core-spring-boot-starter
</artifactId>
<version>2.0.2
</version>
</dependency>
三、TODO List:
准备基于 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>
<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
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:
store-type: none
timeout: PT300s
thymeleaf:
encoding: utf
-8
prefix: classpath
:/templates/
suffix: .htm
servlet:
content-type: text/html;charset=UTF
-8
ums:
oauth:
enabled: true
github:
client-id: 4d4ee00e82f669f2ea8d
client-secret: 953ddbe871a08d6924053531e89ecc01d87195a8
gitee:
client-id: dcc38c801ee88f43cfc1d5c52ec579751c12610c37b87428331bd6694056648e
client-secret: e60a110a2f6e7c930c2d416f802bec6061e19bfa0ceb0df9f6b182b05d8f5a58
auth-login-url-prefix: /auth2/authorization
redirect-url-prefix: /auth2/login
domain: http
://localhost
:9090
proxy:
timeout: PT3S
foreign-timeout: PT150S
client:
login-process-type: redirect
login-page: /login
failure-url: /login
success-url: /
logout-url: /logout
logout-success-url: /login
ignoring-urls:
- /static/**
permit-urls:
- /hello
:GET
- /login
usernameParameter: username
passwordParameter: password
codes:
image:
auth-urls:
- /authentication/form
request-param-image-code-name: imageCode
sms:
auth-urls:
- /authentication/mobile
request-param-mobile-name: mobile
request-param-sms-code-name: smsCode
mobile:
login:
sms-code-login-is-open: true
login-processing-url-mobile: /authentication/mobile
sign:
charset: UTF_8
sign-key-prefix: 'u:sign:'
total-sign-key-prefix: 'total:sign:'
last-few-days: 7
total-expired: 5356800
user-expired: 2678400
---
spring:
profiles: dev
mvc:
throw-exception-if-no-handler-found: true
thymeleaf:
cache: false
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
;
@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
);
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
);
UserDetails user
= User
.builder()
.username(username
)
.password(encodedPassword
)
.disabled(false)
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.authorities(grantedAuthorities
)
.build();
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
{
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
;
@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>
<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>
<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>
<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,
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);
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}"/>
<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。
五、接口使用说明:
实现对应功能时需要实现的接口:
用户服务: 必须实现
UmsUserDetailsService 图片验证码: 如果不实现就会使用默认图片验证码, 实时产生验证码图片, 没有缓存功能
ImageCodeFactory 短信验证码: 默认空实现
SmsCodeSender 滑块验证码: 如果不实现就会使用默认滑块验证码, 实时产生验证码图片, 没有缓存功能
SimpleSliderCodeFactory 自定义验证码:
AbstractValidateCodeProcessorValidateCodeGenerator 访问权限控制功能: 基于 RBAC 的访问权限控制, 增加了更加细粒度的权限控制, 支持 restfulApi; 如: 对菜单与按钮的权限控制
AbstractUriAuthorizeService:
AbstractUriAuthorizeService 类中的方法getRolesAuthorities(); getRolesAuthorities()返回值: Map<role, Map<uri, UriResourcesDTO>> 中UriResourcesDTO字段 uri 与 permission 必须有值.默认实现了 hasPermission(..) 表达式, 实现 AbstractUriAuthorizeService 即生效,
默认启用 httpSecurity.authorizeRequests().anyRequest().access(“hasPermission(request, authentication)”); 方式.如果开启注解方式( @UriAuthorize 或 @EnableGlobalMethodSecurity(prePostEnabled = true) ): 则通过注解 @PerAuthority (“hasPermission(’/users/’, '/users/:list’)”) 方式生效.
六、Configurations:
功能模块demo模块–简单配置demo模块–详细配置
1. 基本功能corebasic-example2. 登录路由功能corebasic-detail-example3. sessioncoresession-detail-example4. remember-mecorebasic-detail-example5. csrfcorebasic-detail-example6. anonymouscorebasic-detail-example7. 验证码corevalidate-code-example8. 手机登录corebasic-detail-example9. 第三方登录corebasic-detail-example10. 给第三方登录时用的数据库表 user_connection 与 auth_token 添加 redis cachecorebasic-detail-example11. 签到corebasic-detail-example12. 基于 RBAC 的访问权限控制功能corepermission-example13. 线程池配置corejustAuth-security-oauth2-example
七、注意事项:
1. 基于 RBAC 的 uri 访问权限控制
更新角色权限时必须调用 AbstractUriAuthorizeService#updateRolesAuthorities() 方法来刷新权限, 即可实时刷新角色权限.
刷新权限有两种方式:一种发布事件,另一种是直接调用服务;推荐用发布事件(异步执行)。
推荐用发布事件(异步执行) applicationContext.publishEvent(new UpdateRolesAuthoritiesEvent(true));直接调用服务 abstractUriAuthorizeService.updateRolesAuthorities();
2. HttpSecurity 配置问题:UMS 中的 HttpSecurityAware 配置与应用中的 HttpSecurity 配置冲突问题:
如果是新建应用添加 HttpSecurity 配置, 通过下面的接口即可:
HttpSecurityAware 如果是已存在的应用:
添加 HttpSecurity 配置, 通过下面的接口即可: HttpSecurityAware已有的 HttpSecurity 配置, 让原有的 HttpSecurity 配置实现此接口进行配置: top.dcenter.security.core.api.config.HttpSecurityAware
3. 在 ServletContext 中存储的属性:
属性名称: SecurityConstants.SERVLET_CONTEXT_AUTHORIZE_REQUESTS_MAP_KEY属性值: authorizeRequestMap<String, Set>: 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
八、属性配置列表
属性配置列表
基本属性签到属性手机登录属性验证码属性第三方授权登录线程池属性第三方授权登录用户信息数据 redis 缓存配置第三方授权登录用户信息表 user_connection sql 配置过时:social_userConnection redisCache属性过时:social属性
九、参与贡献
Fork 本项目新建 Feat_xxx 分支提交代码新建 Pull Request
十、流程图: 随着版本迭代会有出入
1. 滑块验证码
七、时序图(Sequence Diagram)
crsf
getValidateCode
ImageValidateCodeLogin
logout
rememberMe
scurityConfigurer
securityRouter
session
SmsCodeLogin