在这篇文章中,我们将为您详细介绍SpringSecurity权限管理的投票器与表决机制的内容,并且讨论关于springsecurity权限控制的相关问题。此外,我们还会涉及一些关于34.SpringS
在这篇文章中,我们将为您详细介绍Spring Security 权限管理的投票器与表决机制的内容,并且讨论关于spring security 权限控制的相关问题。此外,我们还会涉及一些关于34.SpringSecurity-SpringSecurity Oauth权限表达式、Java Spring Boot Security权限管理秘籍:控制谁可以做什么、Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十五):Spring Security 版本、Spring Boot + Spring Cloud 实现权限管理系统 (Spring Security 版本 )的知识,以帮助您更全面地了解这个主题。
本文目录一览:- Spring Security 权限管理的投票器与表决机制(spring security 权限控制)
- 34.SpringSecurity-SpringSecurity Oauth权限表达式
- Java Spring Boot Security权限管理秘籍:控制谁可以做什么
- Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十五):Spring Security 版本
- Spring Boot + Spring Cloud 实现权限管理系统 (Spring Security 版本 )
Spring Security 权限管理的投票器与表决机制(spring security 权限控制)
今天咱们来聊一聊 Spring Security 中的表决机制与投票器。
当用户想访问 Spring Security 中一个受保护的资源时,用户具备一些角色,该资源的访问也需要一些角色,在比对用户具备的角色和资源需要的角色时,就会用到投票器和表决机制。
当用户想要访问某一个资源时,投票器根据用户的角色投出赞成或者反对票,表决方式则根据投票器的结果进行表决。
在 Spring Security 中,默认提供了三种表决机制,当然,我们也可以不用系统提供的表决机制和投票器,而是完全自己来定义,这也是可以的。
本文松哥将和大家重点介绍三种表决机制和默认的投票器。
1.投票器
先来看投票器。
在 Spring Security 中,投票器是由 AccessDecisionVoter 接口来规范的,我们来看下 AccessDecisionVoter 接口的实现:
可以看到,投票器的实现有好多种,我们可以选择其中一种或多种投票器,也可以自定义投票器,默认的投票器是 WebExpressionVoter。
我们来看 AccessDecisionVoter 的定义:
public interface AccessDecisionVoter<s> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<!--?--> clazz);
int vote(Authentication authentication, S object,
Collection<configattribute> attributes);
}
我稍微解释下:
- 首先一上来定义了三个常量,从常量名字中就可以看出每个常量的含义,1 表示赞成;0 表示弃权;-1 表示拒绝。
- 两个 supports 方法用来判断投票器是否支持当前请求。
- vote 则是具体的投票方法。在不同的实现类中实现。三个参数,authentication 表示当前登录主体;object 是一个 ilterInvocation,里边封装了当前请求;attributes 表示当前所访问的接口所需要的角色集合。
我们来分别看下几个投票器的实现。
1.1 RoleVoter
RoleVoter 主要用来判断当前请求是否具备该接口所需要的角色,我们来看下其 vote 方法:
public int vote(Authentication authentication, Object object,
Collection<configattribute> attributes) {
if (authentication == null) {
return ACCESS_DENIED;
}
int result = ACCESS_ABSTAIN;
Collection<!--? extends GrantedAuthority--> authorities = extractAuthorities(authentication);
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
这个方法的判断逻辑很简单,如果当前登录主体为 null,则直接返回 ACCESS_DENIED 表示拒绝访问;否则就从当前登录主体 authentication 中抽取出角色信息,然后和 attributes 进行对比,如果具备 attributes 中所需角色的任意一种,则返回 ACCESS_GRANTED 表示允许访问。例如 attributes 中的角色为 [a,b,c],当前用户具备 a,则允许访问,不需要三种角色同时具备。
另外还有一个需要注意的地方,就是 RoleVoter 的 supports 方法,我们来看下:
public class RoleVoter implements AccessDecisionVoter<object> {
private String rolePrefix = "ROLE_";
public String getRolePrefix() {
return rolePrefix;
}
public void setRolePrefix(String rolePrefix) {
this.rolePrefix = rolePrefix;
}
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null)
&& attribute.getAttribute().startsWith(getRolePrefix())) {
return true;
}
else {
return false;
}
}
public boolean supports(Class<!--?--> clazz) {
return true;
}
}
可以看到,这里涉及到了一个 rolePrefix 前缀,这个前缀是 ROLE_
,在 supports 方法中,只有主体角色前缀是 ROLE_
,这个 supoorts 方法才会返回 true,这个投票器才会生效。
1.2 RoleHierarchyVoter
RoleHierarchyVoter 是 RoleVoter 的一个子类,在 RoleVoter 角色判断的基础上,引入了角色分层管理,也就是角色继承,关于角色继承,小伙伴们可以参考松哥之前的文章(Spring Security 中如何让上级拥有下级的所有权限?)。
RoleHierarchyVoter 类的 vote 方法和 RoleVoter 一致,唯一的区别在于 RoleHierarchyVoter 类重写了 extractAuthorities 方法。
@Override
Collection<!--? extends GrantedAuthority--> extractAuthorities(
Authentication authentication) {
return roleHierarchy.getReachableGrantedAuthorities(authentication
.getAuthorities());
}
角色分层之后,需要通过 getReachableGrantedAuthorities 方法获取实际具备的角色,具体请参考:Spring Security 中如何让上级拥有下级的所有权限? 一文。
1.3 WebExpressionVoter
这是一个基于表达式权限控制的投票器,松哥后面专门花点时间和小伙伴们聊一聊基于表达式的权限控制,这里我们先不做过多展开,简单看下它的 vote 方法:
public int vote(Authentication authentication, FilterInvocation fi,
Collection<configattribute> attributes) {
assert authentication != null;
assert fi != null;
assert attributes != null;
WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
if (weca == null) {
return ACCESS_ABSTAIN;
}
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
fi);
ctx = weca.postProcess(ctx, fi);
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;
}
如果你熟练使用 SpEL 的话,这段代码应该说还是很好理解的,不过根据我的经验,实际工作中用到 SpEL 场景虽然有,但是不多,所以可能有很多小伙伴并不了解 SpEL 的用法,这个需要小伙伴们自行复习下,我也给大家推荐一篇还不错的文章:https://www.cnblogs.com/larryzeal/p/5964621.html。
这里代码实际上就是根据传入的 attributes 属性构建 weca 对象,然后根据传入的 authentication 参数构建 ctx 对象,最后调用 evaluateAsBoolean 方法去判断权限是否匹配。
上面介绍这三个投票器是我们在实际开发中使用较多的三个。
1.4 其他
另外还有几个比较冷门的投票器,松哥也稍微说下,小伙伴们了解下。
Jsr250Voter
处理 Jsr-250 权限注解的投票器,如 @PermitAll
,@DenyAll
等。
AuthenticatedVoter
AuthenticatedVoter 用于判断 ConfigAttribute 上是否拥有 IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY 三种角色。
IS_AUTHENTICATED_FULLY 表示当前认证用户必须是通过用户名/密码的方式认证的,通过 RememberMe 的方式认证无效。
IS_AUTHENTICATED_REMEMBERED 表示当前登录用户必须是通过 RememberMe 的方式完成认证的。
IS_AUTHENTICATED_ANONYMOUSLY 表示当前登录用户必须是匿名用户。
当项目引入 RememberMe 并且想区分不同的认证方式时,可以考虑这个投票器。
AbstractAclVoter
提供编写域对象 ACL 选项的帮助方法,没有绑定到任何特定的 ACL 系统。
PreInvocationAuthorizationAdviceVoter
使用 @PreFilter 和 @PreAuthorize 注解处理的权限,通过 PreInvocationAuthorizationAdvice 来授权。
当然,如果这些投票器不能满足需求,也可以自定义。
2.表决机制
一个请求不一定只有一个投票器,也可能有多个投票器,所以在投票器的基础上我们还需要表决机制。
表决相关的类主要是三个:
- AffirmativeBased
- ConsensusBased
- UnanimousBased
他们的继承关系如上图。
三个决策器都会把项目中的所有投票器调用一遍,默认使用的决策器是 AffirmativeBased。
三个决策器的区别如下:
- AffirmativeBased:有一个投票器同意了,就通过。
- ConsensusBased:多数投票器同意就通过,平局的话,则看 allowIfEqualGrantedDeniedDecisions 参数的取值。
- UnanimousBased 所有投票器都同意,请求才通过。
这里的具体判断逻辑比较简单,松哥就不贴源码了,感兴趣的小伙伴可以自己看看。
3.在哪里配置
当我们使用基于表达式的权限控制时,像下面这样:
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().fullyAuthenticated()
那么默认的投票器和决策器是在 AbstractInterceptUrlConfigurer#createDefaultAccessDecisionManager 方法中配置的:
private AccessDecisionManager createDefaultAccessDecisionManager(H http) {
AffirmativeBased result = new AffirmativeBased(getDecisionVoters(http));
return postProcess(result);
}
List<accessdecisionvoter<?>> getDecisionVoters(H http) {
List<accessdecisionvoter<?>> decisionVoters = new ArrayList<>();
WebExpressionVoter expressionVoter = new WebExpressionVoter();
expressionVoter.setExpressionHandler(getExpressionHandler(http));
decisionVoters.add(expressionVoter);
return decisionVoters;
}
这里就可以看到默认的决策器和投票器,并且决策器 AffirmativeBased 对象创建好之后,还调用 postProcess 方法注册到 Spring 容器中去了,结合松哥本系列前面的文章,大家知道,如果我们想要修改该对象就非常容易了:
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().fullyAuthenticated()
.withObjectPostProcessor(new ObjectPostProcessor<affirmativebased>() {
@Override
public <o extends affirmativebased> O postProcess(O object) {
List<accessdecisionvoter<?>> decisionVoters = new ArrayList<>();
decisionVoters.add(new RoleHierarchyVoter(roleHierarchy()));
AffirmativeBased affirmativeBased = new AffirmativeBased(decisionVoters);
return (O) affirmativeBased;
}
})
.and()
.csrf()
.disable();
这里只是给大家一个演示,正常来说我们是不需要这样修改的。当我们使用不同的权限配置方式时,会有自动配置对应的投票器和决策器。或者我们手动配置投票器和决策器,如果是系统配置好的,大部分情况下并不需要我们修改。
4.小结
本文主要和小伙伴们简单分享一下 Spring Security 中的投票器和决策器,关于授权的更多知识,松哥下篇文章继续和小伙伴们细聊。</accessdecisionvoter<?></o></affirmativebased></accessdecisionvoter<?></accessdecisionvoter<?></configattribute></object></configattribute></configattribute></s>
34.SpringSecurity-SpringSecurity Oauth权限表达式
前言
- 之前我们看了Spring Security在控制授权这一块他的核心代码,我们看到最后请求通过或者不通过都是转成了权限的表达式,然后交给了一个WebExpressionVoter去评估你的表达式。这个表达式评估的结果是true,那么访问就通过,如果评估的结果是false,那么访问就不通过。那么问题来了,SpringSecurity有多少种权限表达式?每一种权限表达式的写法是怎样的?代表的意思是什么?
内容
1. 表达式说明
上面的表达式都是在WebSecurityConfig可以配置的: 每一个表达式都是对应了HttpSecurity的一个方法,并且是跟在antMatchers之后的。
比如:antMatchers("xxx").permitAll();
antMatchers---指定url
表达式---指定授权
如果需要多个表达式合并统一起来,需要自己通过.access来自己定义:
另一个需求是我们能不能通过.access自定义逻辑:让系统读取我们自己的逻辑而不是使用系统的默认逻辑。
2. 剥离用户自己模块的url服务
2.1 抽离思想
目前我们的安全配置是写在我们的安全模块代码spring-security-core里面的,不管是spring-security-web和spring-security-app.都有一些针对url的安全配置。但是问题是这些url有些是我们的安全模块提供的 ,比如说一下url:
都是安全模块提供的。
但是用户注册("/user/register"),和获取用户的url("/user/*"),他其实是我们的spring-security-demo项目提供的。也就是使用我们安全模块的人提供的安全服务。对于安全模块来说,事先我们并不知道谁会使用我们的安全模块。我也不知道使用此安全模块的url服务。所以针对于使用安全模块的url服务我们应该剥离出去。
实现思路很简单,我们提供一个接口,在我们自己的权限模块里面去实现这个接口(把和安全模块相关的配置写到这个接口里面)。如果是用户模块的话,用户模块自己去实现此接口(将用户模块的url写到用户模块中) 如果我们应用A都需要多有的url服务,那么我们就让应用A去依赖这两个模块。应用A也有这个AuthorizeConfigProvider的实现。所以在应用A的spring容器中 一共有3个模块的权限配置提供者的实现:他们是权限模块、用户模块、应用A实现模块。
1.权限模块:权限模块自身的url权限配置
2.用户模块:用户模块的url权限配置
3.应用A实现模块:应用A特有的url权限配置
最后,在我们的权限模块core中只需要提供一个:AuthorizeConfigManager类,这个类的作用是:把Spring容器里面所有AuthorizeConfigProvider接口的实现全部收集起来。然后按照各个实现的配置给配置好。
假如我们现在有一个应用B,应用B也有自己的AuthorizeConfigProvider的实现并且依赖了权限模块实现、用户模块实现,此时AuthorizeConfigManager类也会收集起三个模块:权限模块、用户模块、应用A实现模块。最终应用A和应用B权限是不同的,但是我们的权限模块core是不管的。
2.2 抽离-代码实现
2.2.1 spring-security-core创建AuthorizeConfigProvider
- 我们创建包:com.yxm.security.core.authorize,然后在此包下创建接口:AuthorizeConfigProvider
- 我们从spring-security-web的WebSecurityConfig的配置里面获取授权url开始的权限对象:
- authorizeRequests()返回的对象是:
-
我们将其封装到授权配置provider中去:
public interface AuthorizeConfigProvider { void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config); }
2.2.2 core实现AuthorizeConfigProvider
MyAuthorizeConfigProvider实现AuthorizeConfigProvider 接口,并申明为Spring的组件。
@Component
public class MyAuthorizeConfigProvider implements AuthorizeConfigProvider {
@Autowired
private SecurityProperties securityProperties;
@Override
public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
config.antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
securityProperties.getBrowser().getLoginPage(),
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*",
securityProperties.getBrowser().getSignUpUrl())
.permitAll();
}
}
2.2.3 core中定义AuthorizeConfigManager
public interface AuthorizeConfigManager {
void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
}
2.2.4 core中定义AuthorizeConfigManager的实现:MyAuthorizeConfigManager
@Component
public class MyAuthorizeConfigManager implements AuthorizeConfigManager {
/**
* 作用:把系统的provider全部收集起来
* @param config
*/
@Autowired
private Set<AuthorizeConfigProvider> authorizeConfigProviders;
@Override
public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
for (AuthorizeConfigProvider authorizeConfigProvider:authorizeConfigProviders){
authorizeConfigProvider.configure(config);
}
//除了上面配置的所有权限外,其他请求都需要授权
config.anyRequest().authenticated();
}
}
2.2.5 web里面改造
@Configuration
public class WebSecurityConfig extends AbstractChannelSecurityConfig {
@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;//验证码过滤器配置
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; //短信验证码授权配置
@Autowired
private SpringSocialConfigurer mySocialSecurityConfig;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SecurityProperties securityProperties;
@Autowired
private DataSource dataSource;
@Autowired
private AuthorizeConfigManager authorizeConfigManager;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
//
//因为是Jdbc操作,所以我们需要注入数据源:org.springframework.jdbc.core.support.JdbcDaoSupport
//tokenRepository继承org.springframework.jdbc.core.support.JdbcDaoSupport
tokenRepository.setDataSource(dataSource);
System.out.println("PersistentTokenRepository--dataSource:>dataSource");
//tokenRepository.setCreateTableOnStartup(true);//系统启动的时候创建:CREATE_TABLE_SQL表
return tokenRepository;
}
/**
* 定义web安全配置类:覆盖config方法
* 1.参数为HttpSecurity
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
applyPasswordAuthenticationConfig(http);
http.apply(validateCodeSecurityConfig)
.and()
.apply(smsCodeAuthenticationSecurityConfig)
.and()
.apply(mySocialSecurityConfig)//配置第三方social
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())//配置token失效秒数
.userDetailsService(userDetailsService)
.and()
.csrf().disable();
authorizeConfigManager.configure(http.authorizeRequests());
}
}
2.2.6 spring-security-app里面改造
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; //短信验证码授权配置
@Autowired
private SpringSocialConfigurer mySocialSecurityConfig;
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;//验证码过滤器配置
@Autowired
private OpenIdAuthenticationSecurityConfig openIdAuthenticationSecurityConfig;
@Autowired
private AuthorizeConfigManager authorizeConfigManager;
@Override
public void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM)
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler);
http.apply(validateCodeSecurityConfig)
.and()
.apply(smsCodeAuthenticationSecurityConfig)
.and()
.apply(mySocialSecurityConfig)//配置第三方social
.and()
.apply(openIdAuthenticationSecurityConfig)
.and()
.csrf().disable();
authorizeConfigManager.configure(http.authorizeRequests());
}
}
2.2.7 spring-security-demo改成
除了core里面的配置,其他用户注册和用户获取的授权都是demo项目自己的配置。
首先先实现:AuthorizeConfigProvider的类:DemoAuthorizeConfigProvider
@Component
public class DemoAuthorizeConfigProvider implements AuthorizeConfigProvider {
@Override
public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
config.antMatchers("/user").hasRole("ADMIN");
}
}
然后在授权时候返回的用户配置上即可:
3.测试
我们访问登录页面http://127.0.0.1:8088/login.html
访问:http://127.0.0.1:8088/user时候:
Java Spring Boot Security权限管理秘籍:控制谁可以做什么
Java Spring Boot Security权限管理是开发中不可或缺的重要组成部分,掌握权限控制是保障系统安全的关键。在这篇文章中,php小编柚子将为您揭秘权限管理的秘籍,帮助您有效控制用户在系统中的操作权限,确保只有合适的人员可以做出相应的操作,从而提高系统的安全性和稳定性。
在Spring Boot Security中,权限管理是一个非常重要的任务。它可以控制用户在应用程序中可以执行的操作。Spring Boot Security提供了两种主要的权限管理机制:基于角色的权限管理(RBAC)和基于表达式的权限管理。
基于角色的权限管理(RBAC)
RBAC是一种经典的权限管理机制。它将用户划分为不同的角色,每个角色具有不同的权限。当用户请求访问应用程序中的资源时,Spring Boot Security会根据用户的角色来判断用户是否具有访问该资源的权限。
立即学习“Java免费学习笔记(深入)”;
基于表达式的权限管理
基于表达式的权限管理是一种更加灵活的权限管理机制。它允许开发人员使用SpEL表达式来定义权限。SpEL表达式是一种强大的表达式语言,它可以用来访问应用程序中的任何数据。
演示代码
// 基于角色的权限管理 @RolesAllowed({ "ROLE_ADMIN" }) public void someAdminMethod() { // 只允许具有ADMIN角色的用户访问此方法 } // 基于表达式的权限管理 @PreAuthorize("hasRole("ROLE_ADMIN") and hasPermission("read", "someEntity")") public void someAdminMethodWithPermission() { // 只允许具有ADMIN角色并且具有对someEntity的read权限的用户访问此方法 }
最佳实践
以下是Spring Boot Security权限管理的一些最佳实践:
- 尽可能使用基于表达式的权限管理。基于表达式的权限管理更加灵活,并且可以更好地适应应用程序的需求。
- 将权限管理与应用程序的业务逻辑分离开来。这将使应用程序更加易于维护和扩展。
- 使用Spring Boot Security提供的各种安全特性,如CSRF保护、XSS过滤等。这将有助于提高应用程序的安全性。
总结
Spring Boot Security权限管理是一个非常重要的任务。它可以控制用户在应用程序中可以执行的操作。本文提供了Spring Boot Security权限管理的秘籍,帮助你轻松实现对用户访问的控制。
以上就是Java Spring Boot Security权限管理秘籍:控制谁可以做什么的详细内容,更多请关注php中文网其它相关文章!
Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十五):Spring Security 版本
在线演示
演示地址:http://139.196.87.48:9002/kitty
用户名:admin 密码:admin
技术背景
到目前为止,我们使用的权限认证框架是 Shiro,虽然 Shiro 也足够好用并且简单,但对于 Spring 官方主推的安全框架 Spring Security,用户群也是甚大的,所以我们这里把当前的代码切分出一个 shiro-cloud 分支,作为 Shiro + Spring Cloud 技术的分支代码,dev 和 master 分支将替换为 Spring Security + Spring Cloud 的技术栈,并在后续计划中集成 Spring Security OAuth2 实现单点登录功能。
代码实现
Maven依赖
移除shiro依赖,添加Spring Scurity和JWT依赖包,jwt目前的最新版本是0.9.1。
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
权限注解
替换Shiro的权限注解为Spring Security的权限注解。
格式如下:
@PreAuthorize("hasAuthority(''sys:menu:view'')")
SysMenuController.java
package com.louis.kitty.admin.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.louis.kitty.admin.model.SysMenu;
import com.louis.kitty.admin.sevice.SysMenuService;
import com.louis.kitty.core.http.HttpResult;
/**
* 菜单控制器
* @author Louis
* @date Oct 29, 2018
*/
@RestController
@RequestMapping("menu")
public class SysMenuController {
@Autowired
private SysMenuService sysMenuService;
@PreAuthorize("hasAuthority(''sys:menu:add'') AND hasAuthority(''sys:menu:edit'')")
@PostMapping(value="/save")
public HttpResult save(@RequestBody SysMenu record) {
return HttpResult.ok(sysMenuService.save(record));
}
@PreAuthorize("hasAuthority(''sys:menu:delete'')")
@PostMapping(value="/delete")
public HttpResult delete(@RequestBody List<SysMenu> records) {
return HttpResult.ok(sysMenuService.delete(records));
}
@PreAuthorize("hasAuthority(''sys:menu:view'')")
@GetMapping(value="/findNavTree")
public HttpResult findNavTree(@RequestParam String userName) {
return HttpResult.ok(sysMenuService.findTree(userName, 1));
}
@PreAuthorize("hasAuthority(''sys:menu:view'')")
@GetMapping(value="/findMenuTree")
public HttpResult findMenuTree() {
return HttpResult.ok(sysMenuService.findTree(null, 0));
}
}
Spring Security注解默认是关闭的,可以通过在配置类添加以下注解开启。
@EnableGlobalMethodSecurity(prePostEnabled = true)
安全配置
添加安全配置类, 继承 WebSecurityConfigurerAdapter,配置URL验证策略和相关过滤器以及自定义的登录验证组件。
WebSecurityConfig.java
package com.louis.kitty.admin.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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import com.louis.kitty.admin.security.JwtAuthenticationFilter;
import com.louis.kitty.admin.security.JwtAuthenticationProvider;
/**
* Spring Security Config
* @author Louis
* @date Nov 20, 2018
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义身份验证组件
auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
http.cors().and().csrf().disable()
.authorizeRequests()
// 跨域预检请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// web jars
.antMatchers("/webjars/**").permitAll()
// 查看SQL监控(druid)
.antMatchers("/druid/**").permitAll()
// 首页和登录页面
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
// swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
// 验证码
.antMatchers("/captcha.jpg**").permitAll()
// 服务监控
.antMatchers("/actuator/**").permitAll()
// 其他所有请求需要身份认证
.anyRequest().authenticated();
// 退出登录处理器
http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
// 登录认证过滤器
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
登录验证组件
继承 DaoAuthenticationProvider, 实现自定义的登录验证组件,覆写密码验证逻辑。
JwtAuthenticationProvider.java
package com.louis.kitty.admin.security;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.louis.kitty.admin.util.PasswordEncoder;
/**
* 身份验证提供者
* @author Louis
* @date Nov 20, 2018
*/
public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
setUserDetailsService(userDetailsService);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
String salt = ((JwtUserDetails) userDetails).getSalt();
// 覆写密码验证逻辑
if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
用户认证信息查询组件
实现 UserDetailsService 接口,定义用户认证信息查询组件,用于获取认证所需的用户信息和授权信息。
UserDetailsServiceImpl.java
package com.louis.kitty.admin.security;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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 com.louis.kitty.admin.model.SysUser;
import com.louis.kitty.admin.sevice.SysUserService;
/**
* 用户登录认证信息查询
* @author Louis
* @date Nov 20, 2018
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.findByName(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
// 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority(''sys:menu:view'')") 标注的接口对比,决定是否可以调用接口
Set<String> permissions = sysUserService.findPermissions(user.getName());
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);
}
}
用户认证信息封装
上面 UserDetailsService 查询的信息需要封装到实现 UserDetails 接口的封装对象里。
JwtUserDetails.java
package com.louis.kitty.admin.security;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* 安全用户模型
* @author Louis
* @date Nov 20, 2018
*/
public class JwtUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private String salt;
private Collection<? extends GrantedAuthority> authorities;
JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.salt = salt;
this.authorities = authorities;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
public String getSalt() {
return salt;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
登录接口
因为我们没有使用内置的 formLogin 登录处理过滤器,所以需要调用登录认证流程,修改登录接口,加入系统登录认证调用。
SysLoginController.java
/**
* 登录接口
*/
@PostMapping(value = "/login")
public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
String username = loginBean.getAccount();
String password = loginBean.getPassword();
String captcha = loginBean.getCaptcha();...
// 系统登录认证
JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
return HttpResult.ok(token);
}
Spring Security 的登录认证过程是通过调用 AuthenticationManager 的 authenticate(token) 方法实现的。
登录流程中主要是返回一个认证好的 Authentication 对象,然后保存到上下文供后续进行授权的时候使用。
登录认证成功之后,会利用JWT生成 token 返回给客户端,后续的访问都需要携带此 token 来进行认证。
SecurityUtils.java
/**
* 系统登录认证
* @param request
* @param username
* @param password
* @param authenticationManager
* @return
*/
public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 执行登录认证过程
Authentication authentication = authenticationManager.authenticate(token);
// 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌并返回给客户端
token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}
令牌生成器
令牌生成器主要是利用JWT生成所需的令牌,部分代码如下。
JwtTokenUtils.java
/**
* JWT工具类
* @author Louis
* @date Nov 20, 2018
*/
public class JwtTokenUtils implements Serializable {
/**
* 生成令牌
* @param userDetails 用户
* @return 令牌
*/
public static String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, SecurityUtils.getUsername(authentication));
claims.put(CREATED, new Date());
claims.put(AUTHORITIES, authentication.getAuthorities());
return generateToken(claims);
}
/**
* 从数据声明生成令牌
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
}
登录认证过滤器
登录认证过滤器继承 BasicAuthenticationFilter,在访问任何URL的时候会被此过滤器拦截,通过调用 SecurityUtils.checkAuthentication(request) 检查登录状态。
JwtAuthenticationFilter.java
package com.louis.kitty.admin.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.louis.kitty.admin.util.SecurityUtils;
/**
* 登录认证过滤器
* @author Louis
* @date Nov 20, 2018
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取token, 并检查登录状态
SecurityUtils.checkAuthentication(request);
chain.doFilter(request, response);
}
}
登录认证检查
登录验证检查是通过 SecurityUtils.checkAuthentication(request) 来完成的。
SecurityUtils.java
/**
* 获取令牌进行认证
* @param request
*/
public static void checkAuthentication(HttpServletRequest request) {
// 获取令牌并根据令牌获取登录认证信息
Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
// 设置登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
上面的登录验证是通过 JwtTokenUtils.getAuthenticationeFromToken(request),来验证令牌并返回登录信息的。
JwtTokenUtils.java
/**
* 根据请求令牌获取登录认证信息
* @param token 令牌
* @return 用户名
*/
public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
Authentication authentication = null;
// 获取请求携带的令牌
String token = JwtTokenUtils.getToken(request);
if(token != null) {
// 请求令牌不能为空
if(SecurityUtils.getAuthentication() == null) {
// 上下文中Authentication为空
Claims claims = getClaimsFromToken(token);
if(claims == null) {
return null;
}
String username = claims.getSubject();
if(username == null) {
return null;
}
if(isTokenExpired(token)) {
return null;
}
Object authors = claims.get(AUTHORITIES);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if (authors != null && authors instanceof List) {
for (Object object : (List) authors) {
authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
}
}
authentication = new JwtAuthenticatioToken(username, null, authorities, token);
} else {
if(validateToken(token, SecurityUtils.getUsername())) {
// 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
authentication = SecurityUtils.getAuthentication();
}
}
}
return authentication;
}
清除Shiro配置
清除掉 config 包下的 ShiroConfig 配置类。
清除 oautho2 包下有关 Shiro 的相关代码。
清除掉 sys_token 表和相关操作代码。
源码下载
后端:https://gitee.com/liuge1988/kitty
前端:https://gitee.com/liuge1988/kitty-ui.git
Spring Boot + Spring Cloud 实现权限管理系统 (Spring Security 版本 )
技术背景
到目前为止,我们使用的权限认证框架是 Shiro,虽然 Shiro 也足够好用并且简单,但对于 Spring 官方主推的安全框架 Spring Security,用户群也是甚大的,所以我们这里把当前的代码切分出一个 shiro-cloud 分支,作为 Shiro + Spring Cloud 技术的分支代码,dev 和 master 分支将替换为 Spring Security + Spring Cloud 的技术栈,并在后续计划中集成 Spring Security OAuth2 实现单点登录功能。
代码实现
Maven依赖
移除shiro依赖,添加Spring Scurity和JWT依赖包,jwt目前的最新版本是0.9.1。

<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>

权限注解
替换Shiro的权限注解为Spring Security的权限注解。
格式如下:
@PreAuthorize("hasAuthority(''sys:menu:view'')")
SysMenuController.java

package com.louis.kitty.admin.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.louis.kitty.admin.model.SysMenu;
import com.louis.kitty.admin.sevice.SysMenuService;
import com.louis.kitty.core.http.HttpResult;
/**
* 菜单控制器
* @author Louis
* @date Oct 29, 2018
*/
@RestController
@RequestMapping("menu")
public class SysMenuController {
@Autowired
private SysMenuService sysMenuService;
@PreAuthorize("hasAuthority(''sys:menu:add'') AND hasAuthority(''sys:menu:edit'')")
@PostMapping(value="/save")
public HttpResult save(@RequestBody SysMenu record) {
return HttpResult.ok(sysMenuService.save(record));
}
@PreAuthorize("hasAuthority(''sys:menu:delete'')")
@PostMapping(value="/delete")
public HttpResult delete(@RequestBody List<SysMenu> records) {
return HttpResult.ok(sysMenuService.delete(records));
}
@PreAuthorize("hasAuthority(''sys:menu:view'')")
@GetMapping(value="/findNavTree")
public HttpResult findNavTree(@RequestParam String userName) {
return HttpResult.ok(sysMenuService.findTree(userName, 1));
}
@PreAuthorize("hasAuthority(''sys:menu:view'')")
@GetMapping(value="/findMenuTree")
public HttpResult findMenuTree() {
return HttpResult.ok(sysMenuService.findTree(null, 0));
}
}

Spring Security注解默认是关闭的,可以通过在配置类添加以下注解开启。
@EnableGlobalMethodSecurity(prePostEnabled = true)
安全配置
添加安全配置类, 继承 WebSecurityConfigurerAdapter,配置URL验证策略和相关过滤器以及自定义的登录验证组件。
WebSecurityConfig.java

package com.louis.kitty.admin.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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import com.louis.kitty.admin.security.JwtAuthenticationFilter;
import com.louis.kitty.admin.security.JwtAuthenticationProvider;
/**
* Spring Security Config
* @author Louis
* @date Nov 20, 2018
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义身份验证组件
auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
http.cors().and().csrf().disable()
.authorizeRequests()
// 跨域预检请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// web jars
.antMatchers("/webjars/**").permitAll()
// 查看SQL监控(druid)
.antMatchers("/druid/**").permitAll()
// 首页和登录页面
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
// swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
// 验证码
.antMatchers("/captcha.jpg**").permitAll()
// 服务监控
.antMatchers("/actuator/**").permitAll()
// 其他所有请求需要身份认证
.anyRequest().authenticated();
// 退出登录处理器
http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
// 登录认证过滤器
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}

登录验证组件
继承 DaoAuthenticationProvider, 实现自定义的登录验证组件,覆写密码验证逻辑。
JwtAuthenticationProvider.java

package com.louis.kitty.admin.security;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.louis.kitty.admin.util.PasswordEncoder;
/**
* 身份验证提供者
* @author Louis
* @date Nov 20, 2018
*/
public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
setUserDetailsService(userDetailsService);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
String salt = ((JwtUserDetails) userDetails).getSalt();
// 覆写密码验证逻辑
if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}

用户认证信息查询组件
实现 UserDetailsService 接口,定义用户认证信息查询组件,用于获取认证所需的用户信息和授权信息。
UserDetailsServiceImpl.java

package com.louis.kitty.admin.security;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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 com.louis.kitty.admin.model.SysUser;
import com.louis.kitty.admin.sevice.SysUserService;
/**
* 用户登录认证信息查询
* @author Louis
* @date Nov 20, 2018
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.findByName(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
// 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority(''sys:menu:view'')") 标注的接口对比,决定是否可以调用接口
Set<String> permissions = sysUserService.findPermissions(user.getName());
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);
}
}

用户认证信息封装
上面 UserDetailsService 查询的信息需要封装到实现 UserDetails 接口的封装对象里。
JwtUserDetails.java

package com.louis.kitty.admin.security;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* 安全用户模型
* @author Louis
* @date Nov 20, 2018
*/
public class JwtUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private String salt;
private Collection<? extends GrantedAuthority> authorities;
JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.salt = salt;
this.authorities = authorities;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
public String getSalt() {
return salt;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}

登录接口
因为我们没有使用内置的 formLogin 登录处理过滤器,所以需要调用登录认证流程,修改登录接口,加入系统登录认证调用。
SysLoginController.java

/**
* 登录接口
*/
@PostMapping(value = "/login")
public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
String username = loginBean.getAccount();
String password = loginBean.getPassword();
String captcha = loginBean.getCaptcha();...
// 系统登录认证
JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
return HttpResult.ok(token);
}

Spring Security 的登录认证过程是通过调用 AuthenticationManager 的 authenticate(token) 方法实现的。
登录流程中主要是返回一个认证好的 Authentication 对象,然后保存到上下文供后续进行授权的时候使用。
登录认证成功之后,会利用JWT生成 token 返回给客户端,后续的访问都需要携带此 token 来进行认证。
SecurityUtils.java

/**
* 系统登录认证
* @param request
* @param username
* @param password
* @param authenticationManager
* @return
*/
public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 执行登录认证过程
Authentication authentication = authenticationManager.authenticate(token);
// 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌并返回给客户端
token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}

令牌生成器
令牌生成器主要是利用JWT生成所需的令牌,部分代码如下。
JwtTokenUtils.java

/**
* JWT工具类
* @author Louis
* @date Nov 20, 2018
*/
public class JwtTokenUtils implements Serializable {
/**
* 生成令牌
* @param userDetails 用户
* @return 令牌
*/
public static String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, SecurityUtils.getUsername(authentication));
claims.put(CREATED, new Date());
claims.put(AUTHORITIES, authentication.getAuthorities());
return generateToken(claims);
}
/**
* 从数据声明生成令牌
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
}

登录认证过滤器
登录认证过滤器继承 BasicAuthenticationFilter,在访问任何URL的时候会被此过滤器拦截,通过调用 SecurityUtils.checkAuthentication(request) 检查登录状态。
JwtAuthenticationFilter.java

package com.louis.kitty.admin.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.louis.kitty.admin.util.SecurityUtils;
/**
* 登录认证过滤器
* @author Louis
* @date Nov 20, 2018
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取token, 并检查登录状态
SecurityUtils.checkAuthentication(request);
chain.doFilter(request, response);
}
}

登录认证检查
登录验证检查是通过 SecurityUtils.checkAuthentication(request) 来完成的。
SecurityUtils.java

/**
* 获取令牌进行认证
* @param request
*/
public static void checkAuthentication(HttpServletRequest request) {
// 获取令牌并根据令牌获取登录认证信息
Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
// 设置登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}

上面的登录验证是通过 JwtTokenUtils.getAuthenticationeFromToken(request),来验证令牌并返回登录信息的。
JwtTokenUtils.java

/**
* 根据请求令牌获取登录认证信息
* @param token 令牌
* @return 用户名
*/
public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
Authentication authentication = null;
// 获取请求携带的令牌
String token = JwtTokenUtils.getToken(request);
if(token != null) {
// 请求令牌不能为空
if(SecurityUtils.getAuthentication() == null) {
// 上下文中Authentication为空
Claims claims = getClaimsFromToken(token);
if(claims == null) {
return null;
}
String username = claims.getSubject();
if(username == null) {
return null;
}
if(isTokenExpired(token)) {
return null;
}
Object authors = claims.get(AUTHORITIES);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if (authors != null && authors instanceof List) {
for (Object object : (List) authors) {
authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
}
}
authentication = new JwtAuthenticatioToken(username, null, authorities, token);
} else {
if(validateToken(token, SecurityUtils.getUsername())) {
// 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
authentication = SecurityUtils.getAuthentication();
}
}
}
return authentication;
}

清除Shiro配置
清除掉 config 包下的 ShiroConfig 配置类。
清除 oautho2 包下有关 Shiro 的相关代码。
清除掉 sys_token 表和相关操作代码。
我们今天的关于Spring Security 权限管理的投票器与表决机制和spring security 权限控制的分享就到这里,谢谢您的阅读,如果想了解更多关于34.SpringSecurity-SpringSecurity Oauth权限表达式、Java Spring Boot Security权限管理秘籍:控制谁可以做什么、Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十五):Spring Security 版本、Spring Boot + Spring Cloud 实现权限管理系统 (Spring Security 版本 )的相关信息,可以在本站进行搜索。
本文标签: