GVKun编程网logo

Spring Security 权限管理的投票器与表决机制(spring security 权限控制)

30

在这篇文章中,我们将为您详细介绍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 权限控制)

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. 首先一上来定义了三个常量,从常量名字中就可以看出每个常量的含义,1 表示赞成;0 表示弃权;-1 表示拒绝。
  2. 两个 supports 方法用来判断投票器是否支持当前请求。
  3. 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)
				&amp;&amp; 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<?>&gt; getDecisionVoters(H http) {
	List<accessdecisionvoter<?>&gt; decisionVoters = new ArrayList&lt;&gt;();
	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<?>&gt; decisionVoters = new ArrayList&lt;&gt;();
                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权限表达式

34.SpringSecurity-SpringSecurity Oauth权限表达式

前言

image.png

  1. 之前我们看了Spring Security在控制授权这一块他的核心代码,我们看到最后请求通过或者不通过都是转成了权限的表达式,然后交给了一个WebExpressionVoter去评估你的表达式。这个表达式评估的结果是true,那么访问就通过,如果评估的结果是false,那么访问就不通过。那么问题来了,SpringSecurity有多少种权限表达式?每一种权限表达式的写法是怎样的?代表的意思是什么?

内容

1. 表达式说明

image.png
上面的表达式都是在WebSecurityConfig可以配置的: 每一个表达式都是对应了HttpSecurity的一个方法,并且是跟在antMatchers之后的。
image.png
比如:antMatchers("xxx").permitAll();
antMatchers---指定url
表达式---指定授权

如果需要多个表达式合并统一起来,需要自己通过.access来自己定义: image.png
另一个需求是我们能不能通过.access自定义逻辑:让系统读取我们自己的逻辑而不是使用系统的默认逻辑。

2. 剥离用户自己模块的url服务

2.1 抽离思想

目前我们的安全配置是写在我们的安全模块代码spring-security-core里面的,不管是spring-security-web和spring-security-app.都有一些针对url的安全配置。但是问题是这些url有些是我们的安全模块提供的 ,比如说一下url:
image.png
都是安全模块提供的。
但是用户注册("/user/register"),和获取用户的url("/user/*"),他其实是我们的spring-security-demo项目提供的。也就是使用我们安全模块的人提供的安全服务。对于安全模块来说,事先我们并不知道谁会使用我们的安全模块。我也不知道使用此安全模块的url服务。所以针对于使用安全模块的url服务我们应该剥离出去。

image.png

实现思路很简单,我们提供一个接口,在我们自己的权限模块里面去实现这个接口(把和安全模块相关的配置写到这个接口里面)。如果是用户模块的话,用户模块自己去实现此接口(将用户模块的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
  1. 我们创建包:com.yxm.security.core.authorize,然后在此包下创建接口:AuthorizeConfigProvider
  2. 我们从spring-security-web的WebSecurityConfig的配置里面获取授权url开始的权限对象:
    image.png
  3. authorizeRequests()返回的对象是:
    image.png
  4. 我们将其封装到授权配置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");
    }
}

然后在授权时候返回的用户配置上即可:

image.png

3.测试

我们访问登录页面http://127.0.0.1:8088/login.html
image.png

访问:http://127.0.0.1:8088/user时候:
image.png

Java Spring Boot Security权限管理秘籍:控制谁可以做什么

Java Spring Boot Security权限管理秘籍:控制谁可以做什么

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

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 版本 )

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 版本 )的相关信息,可以在本站进行搜索。

本文标签: