整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

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)
    && 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 中的投票器和决策器,关于授权的更多知识,松哥下篇文章继续和小伙伴们细聊。

天是被《即刻电音》相关热搜洗版的一天。

上到节目主理人,下到主理人粉丝,甚至连主理人的经纪人都没落下。

可能是节目开播以来喜提热搜最多的一次。

到底怎么回事呢?

原来是《即刻电音》的录制现场发生了极大的骚乱。

来看一个张艺兴粉丝发来的repo▽

简单总结一下:

1.大张伟粉丝觉得节目投票有问题,一直在喊“黑幕”

2.大张伟为保队员又一次打起了感情牌

3.张艺兴想说话却发现话筒没声,愤怒地喊“开麦”和“宇宙(张艺兴队名)就是diao”

4.张艺兴经纪人上场抢走张艺兴话筒,并说大张伟粉丝输不起就别玩

插播一条,给没看过《即刻电音》的朋友们补补课。

张艺兴粉丝控诉的打感情牌其实是指大张伟在节目中哭诉的事。

当时因为有选手对淘汰的规则表示不满,大张伟搬出自己的例子来进行说服。

过程中他一度哽咽地说:“我的心告诉我应该退出这个行业,但我一直都在这个行业的原因就是因为我爱音乐。我有很多问题,但是也有太多人仗着我喜欢音乐欺负我。

用笑遮掩自己的情绪失控就还挺令人动容的▽

OK,补完课再回到两家粉丝撕X的事。

咱们也不能只偏听一方观点,再来看一下大张伟粉丝方面关于录制现场的repo▽

过程上的描述和张艺兴粉丝那边的说法基本相同。

但细节上进行了一些反驳,比如:

1.大张伟粉丝质疑节目组黑幕是因为从第二轮开始就发现投票器有问题

2.大张伟情绪激动是因为他的团队面临团灭的风险,同时质问导演组:“钱我可以不要了,但你们要有底线。”

3.大张伟粉丝骂节目组的话被路过的张艺兴听到了,并回头问大张伟粉丝“谁骂我”

4.张艺兴确实大喊开麦被杨天真拦下了,但当时本来就要退场了,所有人的麦都会被收

也是个公说公有理,婆说婆有理的“罗生门”。

不过,让张艺兴粉丝生气的其中一个原因还在于大张伟粉丝在节目现场上升爱豆。

据张艺兴粉丝描述,当时看张艺兴情绪激动,大张伟粉丝不仅没有收敛,还冷嘲热讽骂张艺兴犯贱。

说:“见过捡钱的,见过捡东西的,没见过捡骂的。”

但大张伟粉丝则说,他们压根没骂张艺兴,骂的是这个XX的世界,结果恰好被路过的张艺兴听到。

由此还衍生出了一个关于张艺兴从大张伟粉丝全世界路过的段子...

更抓马的部分在于,两方粉丝都说是第一次见自家爱豆这么生气。

粉丝的卖惨逻辑就真的都很相似。

大张伟粉丝还痛心疾首称:“我第一次见大张伟当众给自己一个耳光,这要怎么才能下得去手?”

本来张艺兴粉丝就一直在斥责大张伟卖惨了,这么一说不是在给对家送锤送人头吗...

于是,其他粉丝只好一而再再而三地进行澄清。

替他们感到心累。

然而,就在双方争得不可开交之时,张艺兴粉丝却开始一条接一条地甩出录制现场的各种音频。

要知道,节目录制是不能带这些电子设备进去的。

真不愧是经验老道的流量粉。

M@https://v.youku.com/v_show/id_XMzk4OTUxNTQzNg==.html@M

根据音频来看,大张伟确实哽咽了▽

大张伟发言完毕后,他的粉丝用很大的声音喊“黑幕”▽

这时,轮到张艺兴队的队员tsunano进行发言。

他说想给大家讲一个小故事。

但台下却一直情绪激动地在喊“不听”▽

tsunano试图将故事继续讲下去,背景音却全是让他闭嘴的声音▽

这些“不听”、“闭嘴”的抗议声甚至大到大张伟不得不出面调节的程度▽

而在结果公布后,节目组应该是有让主理人进到候场区之类的地方进行休息或是调试。

只听到录制重新开始,导演让主理人回到各自的位置。

就在这时,张艺兴突然大喊了几声开麦。

开麦后,就有了他感慨“wow”。

紧接着就是repo种所说的张艺兴歇斯底里的那句“宇宙队就是最diao的”。

之前也有人说是大张伟煽动了现场的粉丝情绪。

在被曝光的另一段录音当中,有粉丝录到休息期间,大张伟的确带着情绪对张艺兴粉丝 说了“你们牛逼”。

但橘子君听完音频觉得倒也不像一些repo当中说的那样是朝张艺兴粉丝喊话。

当时大张伟并没有带麦,算是正常讲话的声音大小,应该只有附近一部分粉丝能听见。

万万没想到,录个节目竟然能录到两位在娱乐圈摸爬滚打多年的艺人全都情绪失控...

只能说,大家就还挺真情实感的。

听一听音频就知道了,现场氛围真的就跟打群架似的,来一把火随时能着...

面对如此窘境,主持人只好先cue掌握着最主要投票权的专业音乐评审回应黑幕问题▽

看观众情绪依然未得到任和平复,主持人又出面了。

他说:“这个节目没有黑幕,也没必要有黑幕。你觉得你投票器坏了那一票就能够让整个节目的结果有所改变吗?你觉得所有你们投票器坏了就针对你们了吗?”

嗯嗯嗯???

主持人不应该是起到安抚作用吗,怎么所说的话还这么具有煽动性呢???

很多比赛就是会因为一票之差决定命运啊,难道不是每一票都很重要吗???

不过,从专业评审和主持人的两段话来看,《即刻电音》这个节目似乎还真的存在一些问题。

投票器坏掉也就算了,专业评审竟然说自己没有完整看到选手的演出???

到底咋回事啊...

不是说好的公平公正吗[捂脸]...

所以,也怪不得两边粉丝会这样爆发。

连节目中的另一位主理人尚雯婕都在节目录完的第一时间无语地在微博发了个“呵呵”。

再来说说《即刻电音》这个节目。

别看它电音的题材小众,但每期的话题还真不比爆款节目来得少。

第一期就因为选手diss大张伟而引发讨论。

当时,选手Anti-General说自己一听大张伟要来参加节目,他的第一反应就是退赛。

节目一播出,Anti-General就因为这番言论被骂很惨。

随后,他又在微博发了长文进行解释。

为什么会在节目里这样说?

一是因为抄袭事件,二是因为大张伟在节目中所表演的曲目并不能让他认同。

虽然大张伟在节目中的一些专业表现让他的印象有所改观,但他还是认为大张伟在用自己做不到的事要求别人。

他理解大张伟所承受的精神压力,但又认为这些压力都是因为大张伟还没能做出服众的作品,那大家就只能把以前的事翻出来讲。

最后,Anti-General还很呛地放话:至于大老师你这次能不能洗白,我们拭目以待。

当时大张伟也有回应,他说:

“一个常年在做音乐的人,即便不善言表,答起这个‘自己’的问题也会滔滔不绝。”

“我音乐中的自己就是DM48的游乐园,就是阳光彩虹小白马,就是必须热血有趣又可爱。”

算是解答了Anti-General说他“用自己做不到的事要求别人”的这部分▽

但选手所提出的洗白质疑依然依然是目前《即刻电音》为人诟病的问题之一。

大概是因为常常被说抄袭,所以大张伟在《即刻电音》中一直都冲在抓抄袭的第一线。

选手表演完,他立刻就说跟某乐队的音乐有些相似▽

选手承认有借鉴,但并不承认抄袭▽

特邀主理人也认同大张伟说的观点▽

大张伟则表示,自己之所以会说这段话,是因为有时候大家所认为的瑕疵,“天都会原谅你,但是网友不会”。

再加上开头我们放过的那段大张伟哭诉截图,理所当然有不少人会质疑这节目是在给大张伟洗白。

而针对洗白质疑,大张伟自己则说:“我根本没必要洗白,因为我早就是彩色的了。

除此之外,围绕张艺兴的主要争议则是上次让冯提莫晋级。

冯提莫大家应该都并不陌生,印象中她跟电音没扯上过啥关系。

但她却来参加了这档节目...

张艺兴看完表演后很为难,他先是做了这样一个双手抱头的动作,说了一句“嘶...喔...这个......这个怎么说呢”▽

然后给出了这样的评价:

“你们选的是未来感,没有让我觉得很未来啊。”

这时候和冯提莫一起合作的KK张站出来帮冯提莫讲话,说“想要帮她制作和改编歌曲,所以在编曲上用了大量未来的声音设计......”

艺兴终于听不下去了,举手打断:

“等一下,对不起打断你一下,就是我觉得就很普通,一般的电子舞曲的歌曲就是这样的,我没有觉得有什么很未来的东西。”

“你是一个很好的歌手,你也是一个好的制作人,就是呈现出来的东西有点差强人意。”

到现在为止,张艺兴的态度已经很明显了吧,他并不满意这个组合的表演。

按照这个走向,他接下来会很自然地“淘汰”他们才对。

然而最终的结果是,他给了“推荐”(满脸写着开心)▽

粉丝们也看不过去了,在评论里放出了录制当天的“实情”。

说是在导演劝说下张艺兴才会让冯提莫晋级。

但张艺兴自己则说“是我做的决定,导演组没有逼我”。

但也并没有多少人相信就是了。

热评都是“被绑架了你就眨眨眼”...

-------------------------

到目前为止《即刻电音》一共才播了5期,争议话题就已经有前面说的这么多了。

争议程度怕是跟当年的《花少2》都有得一拼了吧?

还不知道最新录制的这一期已经乱到这种程度后期要怎么剪。

吃瓜...

最后一句

能做的不多,给后期买点防脱洗发水吧。

. Spring Security 简介

Spring Security 是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。原名Acegi Security,最早于2003年起源于Spring社区。 2007年末正式归为Spring Framework的正式子项目,并改名为Spring Security 。此后, Spring Security有了长远发展,现已成为一款基于Spring Framework的广泛应用的安全框架,主要为应用服务提供用户认证(Authentication)和用户授权(Authorization)功能。详情可参考Spring Security 官方文档。

Spring Security 针对安全方面的两大难题, 鉴权(Authentication)和授权(Authorization)提供了灵活强大的解决方案。

  • 用户鉴权(Authentication), 是指对用户身份的鉴权, 验证某个用户是否为系统中的合法对象, 是否能够访问对应的系统资源。比如用户输入账户和密码登陆系统。
  • 用户授权(Authorization),是指授予通过认证的用户指定的系统资源操作权限, 能否执行具体某个操作。比如用户能够访问操作的菜单,能够请求的功能接口, 这些都是系统资源。

Spring Security优势:

  • 灵活性, Spring Security并不局限于Spring MVC,虽然它是基于Spring Framework实现的,但它并不依赖于Spring MVC,可以独立于MVC应用在其他Java EE框架之上。
  • 功能强大,Spring Security的安全管制并不只限制于Web请求,除此之外它还可以针对方法调用通过AOP的方式进行安全管制,甚至可以对域对象实例(Domain Object Instance)进行访问控制。
  • 安全保护, 防止伪造身份, Spring Security 会自动拦截站点所有状态变化的请求(非GET,HEAD,OPTIONS和TRACE的请求),防止跨站请求伪造(CSRF防护),即防止其他网站或是程序POST等请求本站点。

如果项目需要安全控制功能,不用自己去实现一套, 集成Spring Security专业安全框架是首选, 适用后台管理、接口资源管理、微服务统一鉴权等场景。

2. Spring Security设计处理机制

处理流程图:


  • 客户端发起一个请求,进入 Security 过滤器链。
  • 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
  • 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
  • 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
  • 投票机制, 三种表决方式, 默认采用一票制(AffirmativeBased

类名

描述

AffirmativeBased

只要有一个投票器允许访问, 请求立刻允许放行, 不管之前存在拒绝的决定

ConsensusBased

多数票机制(允许或拒绝),多数的一方决定了AccessDecisionManager的结果。平局的投票和空票(全部弃权)的结果是可配置的

UnanimousBased

所有的投票器必须全部是允许的, 否则访问将被拒绝

3. Spring Boot 与Spring Security 集成配置

Spring Boot 与Spring Security 集成, 包含一般集成用法, 还包括自定义用户登陆页面使用, 自定义内存模式验证, 以及自定义登陆成功与失败逻辑处理。

1、创建工程spring-boot-security-integrate

启动类:

com.mirson.spring.boot.security.integrate.startup.SecurityIntegrateApplication

@SpringBootApplication
@ComponentScan(basePackages = {"com.mirson"})
public class SecurityIntegrateApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityIntegrateApplication.class, args);
    }
}

2、MAVEN依赖

<dependencies>
        <!-- Spring Boot Security 安全依赖组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Spring Boot Thymeleaf 模板依赖组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- Spring Boot Web 依赖组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--Spring boot freemarker 自动化配置组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
    </dependencies>

3、定义一个外部访问接口

1)创建实体

定义一个用户实体

com.mirson.spring.boot.security.integrate.po.User

@Data
public class User {

    /**
     * ID
     */
    private Integer id;

    /**
     * 用户名称
     */
    private String name;

    /**
     * 年龄
     */
    private String age;

    /**
     * 省份
     */
    private String province;


    /**
     * 创建时间
     */
    private Date createDate;

}

2)提供Web访问接口

提供一个获取用户信息接口

com.mirson.spring.boot.security.integrate.controller.UserController

@RestController
@RequestMapping("/user")
@Log4j2
public class UserController {

    @GetMapping("/getUserInfo")
    @ResponseBody
    public User getUserInfo() {
        User user = new User();
        user.setId(0);
        user.setAge("21");
        user.setName("user1");
        user.setCreateDate(new Date());
        return user;
    }

}

4、工程配置

server:
  port: 22618
spring:
  application:
    name: security-integrate

  # security 安全配置
  security:
    user:
      name: "admin"
      password: "admin"

设置默认的用户名与密码为admin。

5、功能验证

1) 请求获取用户信息接口

访问接口: http://127.0.0.1:22618/getUserInfo

没有鉴权的情况下, 会出现登陆界面。

2)输入用户信息admin/admin, 再次请求获取用户信息接口

正确输入用户信息后, 可以正常访问用户信息接口。


4. Spring Security 自定义鉴权实现

4.1 自定义登陆页面处理

Spring Security 内置会有一套登陆页面, 也可以自定修改, 这里通过freemark模板来实现自定登陆页面渲染。

application.yml增加配置:

   # freemarker 模板配置
     freemarker:
       allow-request-override: false
       allow-session-override: false
       cache: true
       charset: UTF-8
       check-template-location: true
       content-type: text/html
       enabled: true
       expose-request-attributes: false
       expose-session-attributes: false
       expose-spring-macro-helpers: true
       prefer-file-system-access: true
       suffix: .ftl
       template-loader-path: classpath:/templates/
   

增加freemark模板文件:

   <!DOCTYPE html>
   <html lang="en">
     <head>
       <meta charset="utf-8">
       <meta http-equiv="X-UA-Compatible" content="IE=edge">
       <meta name="viewport" content="width=device-width, initial-scale=1">
       <meta name="description" content="">
       <meta name="author" content="">
   
       <title>自定义系统登陆</title>
   
       <link href="/css/bootstrap.min.css" rel="stylesheet">
       <link href="/css/signin.css" rel="stylesheet">
     </head>
   
     <body>
       <div class="container form-margin-top">
         <form class="form-signin" action="/user/doUserLogin" method="post">
           <h2 class="form-signin-heading" align="center">自定义系统登陆</h2>
           <input type="text" name="username" class="form-control form-margin-top" placeholder="账号" required autofocus>
           <input type="password" name="password" class="form-control" placeholder="密码" required>
           <button class="btn btn-lg btn-primary btn-block" type="submit">sign in</button>
         </form>
       </div>
       <footer>
         <p>support by: mirson</p>
       </footer>
     </body>
   </html>
   

添加CSS静态资源文件

添加JAVA CONFIG配置:

com.mirson.spring.boot.security.integrate.config.SpringSecurityConfiguration

   @Configuration
   @EnableWebSecurity
   public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
       @Override
       protected void configure(HttpSecurity http) throws Exception {
          http
                  .authorizeRequests()
                  .antMatchers("/user/userLoginForm",
                          "/css/**").permitAll()
                  .antMatchers("/user/getUserInfo").authenticated() 
                  .and()
                  .formLogin()
                  .loginPage("/user/userLoginForm")    //自定义登录页面             
                  .permitAll()            //允许所有人访问该路由
                  .and()
                  .csrf()
                  .disable()                //暂时禁用csrc否则无法提交
                  .httpBasic();
       }
   }

重启, 访问接口: http://127.0.0.1:22618/user/getUserInfo

会自动跳转到自定义的登陆页面:

4.2 自定义资源访问配置

修改com.mirson.spring.boot.security.integrate.config.SpringSecurityConfiguration配置:

   @Configuration
   @EnableWebSecurity
   public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
       
       @Override
       protected void configure(HttpSecurity http) throws Exception {
          http
                  .authorizeRequests()
                  .antMatchers("/user/doUserLogin", "/user/userLoginForm",
                          "/css/**").permitAll()
                  .antMatchers("/user/getUserInfo").authenticated() //hasRole("ADMIN")
                  .and()
                  .formLogin()
                  .loginPage("/user/userLoginForm")    //自定义登录页面
                  .loginProcessingUrl("/user/doUserLogin") // 自定义登陆处理地址
                  .permitAll()            //允许所有人访问该路由
                  .and()
                  .csrf()
                  .disable()                //暂时禁用csrc否则无法提交
                  .httpBasic();
       }
       
   }
   

通过Spring Security 可以控制, 哪些资源需要受权限保护, 哪些可以开放访问。

  • 开放访问

/user/userLoginForm 用户登陆页面

/user/doUserLogin 登陆处理地址

/css/** 静态资源文件

  • 权限保护

/user/getUserInfo 获取用户信息接口

可以指定Role角色权限, 不指定, 只要登陆即拥有访问权限。

loginPage指定/user/userLoginForm为自定义登陆页面;

loginProcessingUrl为登陆请求处理接口地址,可以不用对其做具体实现,Spring Security 会做默认处理。

4.3 自定义内存模式鉴权

1、创建鉴权用户对象:

com.mirson.spring.boot.security.integrate.po.OAuthUser


@Data
public class OAuthUser extends User {

    public OAuthUser(String account, String password){

        super(account, password, true, true, true, true, Collections.EMPTY_SET);
    }

}

继承的是Spring Security 的User对象, 将账号和密码信息, 传递至父类构造方法。

2、修改SpringSecurityConfiguration配置

增加:

@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    /**
     * 用户认证服务
     * */
    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        //创建基于内存用户管理对象
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        //自定义权限
        Collection<GrantedAuthority> adminAuth = new ArrayList<>();
        adminAuth.add(new SimpleGrantedAuthority("ADMIN"));
        //自定义用户
        OAuthUser oAuthUser = new OAuthUser("admin", "admin123");
        manager.createUser(oAuthUser);
        return manager;
    }
    
    /**
     * 配置密码编码器, 不需加密
     * @return
     */
    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }
    
    ...

}

采用内存模式管理用户对象InMemoryUserDetailsManager, 自定义认证用户名和密码, 分别为admin,admin123。 需要配置密码编码器, 可以支持自定义密码加密方式, 这里不需加密, 配置NoOpPasswordEncoder。

4.4 自定义登陆成功处理器

Spring Security 提供了接口, 登陆成功, 可以通过处理器实现自定义逻辑。 新建com.mirson.spring.boot.security.integrate.handler.SecuritySuccessHandler

@Component
@Log4j2
public class SecuritySuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 认证成功处理
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        log.info("Process in SecuritySuccessHandler ==> login success.");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

这里通过自定义登陆成功处理器, 将登陆成功的信息返回客户端。

将处理器加入到自定义配置SpringSecurityConfiguration中:

	@Override
    protected void configure(HttpSecurity http) throws Exception {
       http
               .authorizeRequests()
               .antMatchers("/user/doUserLogin", "/user/userLoginForm",
                       "/css/**").permitAll()
               .antMatchers("/user/getUserInfo").authenticated() //hasRole("ADMIN")
               .and()
               .formLogin()
               .loginPage("/user/userLoginForm")    //自定义登录页面
               .loginProcessingUrl("/user/doUserLogin") // 自定义登陆处理地址
               .successHandler(securitySuccessHandler)  // 自定义登陆成功处理器
               .permitAll()            //允许所有人访问该路由
               .and()
               .csrf()
               .disable()                //暂时禁用csrc否则无法提交
               .httpBasic();
    }

4.5 自定义登陆失败处理器

如果登陆失败, 也可以通过处理器实现自定义逻辑。 新建com.mirson.spring.boot.security.integrate.handler.SecurityFailureHandler

@Component
@Log4j2
public class SecurityFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {

        log.info("Process in SecurityFailureHandler ==> login failure.");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
    }
}

将登陆失败的错误信息, 返回给客户端。

修改自定义配置SpringSecurityConfiguration:

@Override
    protected void configure(HttpSecurity http) throws Exception {
       http
               .authorizeRequests()
               .antMatchers("/user/doUserLogin", "/user/userLoginForm",
                       "/css/**").permitAll()
               .antMatchers("/user/getUserInfo").authenticated() //hasRole("ADMIN")
               .and()
               .formLogin()
               .loginPage("/user/userLoginForm")    //自定义登录页面
               .loginProcessingUrl("/user/doUserLogin") // 自定义登陆处理地址
               .successHandler(securitySuccessHandler)  // 自定义登陆成功处理器
               .failureHandler(securityFailureHandler)  // 自定义登陆失败处理器
               .permitAll()            //允许所有人访问该路由
               .and()
               .csrf()
               .disable()                //暂时禁用csrc否则无法提交
               .httpBasic();
    }

4.6 自定义鉴权功能验证

1、验证内存模式鉴权

内存模式我们设置的用户名和密码为admin/admin123

默认配置文件是admin/admin

输入admin/admin123, 成功登陆, 内存模式已生效。

2、登陆成功处理器验证

重启服务, 访问获取用户信息接口, http://127.0.0.1:22618/user/getUserInfo

默认是会跳转到登陆页面, 如果没配置登陆成功处理器, 登陆成功后, 会进入上一次访问页面

可以看到, 登陆成功后, 并没有跳转到上一次访问的用户信息接口, 而是返回了登陆成功处理器的结果。

3、登陆失败处理器验证

同样, 问获取用户信息接口, http://127.0.0.1:22618/user/getUserInfo, 会自动跳转到登陆页面。

采用错误的用户密码, 返回了登陆失败处理器的结果。

5. 总结

Spring Security 提供了鉴权与授权的功能支持, 这里做了详细讲解, 如何使用与配置, 并讲解了自定义鉴权处理功能, 实际业务当中,并非一层不变, 会做不同配置修改,比如自定义资源访问配置, 不同项目有不同的要求, 掌握这些自定义配置, 基本可以覆盖主要的业务场景, 针对更复杂的鉴权, 可以采用oauth2做鉴权处理, 在后续教程中会做讲解。

教程源码下载地址: https://download.csdn.net/download/hxx688/86400104