本文主要是介绍基于 springSecurity 的用户管理脚手架(UMS),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
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 数据 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
:
准备基于 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 : devdatasource : driver-class-name : com.mysql.cj.jdbc.Driverurl : jdbc: mysql: //127.0.0.1: 3306/ums? useSSL=false&useUnicode =true&characterEncoding =UTF- 8&zeroDateTimeBehavior =convertToNull&serverTimezone =Asia/Shanghaiusername : rootpassword : 123456 session : store-type : nonetimeout : PT300sthymeleaf : encoding : utf- 8 prefix : classpath: /templates/suffix : .htmservlet : content-type : text/html;charset=UTF- 8
ums : oauth : enabled : true github : client-id : 4d4ee00e82f669f2ea8dclient-secret : 953ddbe871a08d6924053531e89ecc01d87195a8gitee : client-id : dcc38c801ee88f43cfc1d5c52ec579751c12610c37b87428331bd6694056648eclient-secret : e60a110a2f6e7c930c2d416f802bec6061e19bfa0ceb0df9f6b182b05d8f5a58auth-login-url-prefix : /auth2/authorizationredirect-url-prefix : /auth2/logindomain : http: //localhost: 9090 proxy : timeout : PT3Sforeign-timeout : PT150Sclient : login-process-type : redirectlogin-page : /loginfailure-url : /loginsuccess-url : /logout-url : /logoutlogout-success-url : /loginignoring-urls : - /static/**permit-urls : - /hello: GET- /loginusernameParameter : usernamepasswordParameter : passwordcodes : image : auth-urls : - /authentication/formrequest-param-image-code-name : imageCodesms : auth-urls : - /authentication/mobilerequest-param-mobile-name : mobilerequest-param-sms-code-name : smsCodemobile : login : sms-code-login-is-open : true login-processing-url-mobile : /authentication/mobilesign : charset : UTF_8sign-key-prefix : 'u:sign:' total-sign-key-prefix : 'total:sign:' last-few-days : 7 total-expired : 5356800 user-expired : 2678400 ---
spring : profiles : devmvc : 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. targetUrlif ( uri === null ) { uri = basePath} window. location. href = uri; } , error: function ( data) { $ ( errorNameId) . text ( "" ) $ ( errorCodeId) . text ( "" ) console. log ( "********注册失败*********" ) console. log ( data) data = data. responseJSONif ( 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。
五、接口使用说明
:
实现对应功能时需要实现的接口:
用户服务: 必须实现
图片验证码: 如果不实现就会使用默认图片验证码, 实时产生验证码图片, 没有缓存功能 短信验证码: 默认空实现
滑块验证码: 如果不实现就会使用默认滑块验证码, 实时产生验证码图片, 没有缓存功能 自定义验证码: AbstractValidateCodeProcessor ValidateCodeGenerator 访问权限控制功能: 基于 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. 基本功能 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()
方法来刷新权限 , 即可实时刷新角色权限. 刷新权限 有两种方式:一种发布事件,另一种是直接调用服务;推荐用发布事件(异步执行)。 推荐用发布事件(异步执行) applicationContext.publishEvent(new UpdateRolesAuthoritiesEvent(true));
直接调用服务 abstractUriAuthorizeService.updateRolesAuthorities();
如果是新建应用添加 HttpSecurity 配置, 通过下面的接口即可: 如果是已存在的应用: 添加 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.
通过静态方法获取 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
这篇关于基于 springSecurity 的用户管理脚手架(UMS)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!