使用SpringSecurity搭建授权认证服务(1) -- 基本demo认证原理
使用SpringSecurity搭建授权认证服务(1) -- 基本demo
登录认证是做后台开发的最基本的能力,初学就知道一个interceptor或者filter拦截所有请求,然后判断参数是否合理,如此即可。当涉及到某些接口权限的时候,则if-else判断以下,也是没问题的。
但如果判断多了,业务逻辑也掺杂在一起,降低可读性的同时也不利于扩展和维护。于是就出现了apache shiro, spring security这样的框架,抽离出认证授权判断。
由于我现在的项目都是给予springboot的,那选择spring security就方便很多。接下来基于此构建我的认证授权服务: 基于Token的认证授权服务。
项目初始化
第一个版本,项目初始化https://github.com/Ryan-Miao/spring-security-token-login-server/releases/tag/v1.0
首先学习两个单词:
authentication 身份验证
authorized 经授权的
我们登录鉴权就是两个步骤,先认证登录,然后权限校验。对应到Spring Security里就是 AuthenticationManager
和AccessDecisionManager
,前者负责对用户凭证进行认证,后者对认证后的权限进行校验。
首先,创建一个基本的springboot项目。
- 引入Springboot, Mybatis, Redis, Swagger, Spring Security
- 配置全局异常拦截
ExceptionInterceptor
- 配置Redis缓存,这里使用redisson,也可以直接使用starter
- 配置Spring Security Config
Spring Security参照官方文档配置即可。接下来是自定义和可以修改的地方。
数据表权限模型
本项目简单使用 user - role -permission的模型。
- 一个user可以有多个role
- 一个role可以指定给多个user
- 一个role可以拥有多个permission
- 一个permission也可以从属于多个role
权限判定通过判断user是否拥有permission来决定。通过role实现了user和permission之间的解耦,创建多个role模型,绑定对应的权限,当添加新用户的时候,直接指定role就可以授权。
Spring Security自带了org.springframework.security.provisioning.JdbcUserDetailsManager,它里面的模型为user-group-authority. 即用户归属用户组,用户组有权限。差不多可以和当前模型一一对应。
认证流程
大体认证流程和涉及的核心类如下:
ApplicationFilterChain的filter顺序:
FilterChainProxy(springSecurityFilterChain)执行认证的顺序, 忽略的url将不命中任何filter, 而需要认证的url将通过VirtualFilterChain来认证。
使用Token认证
starter默认启用的基于用户名密码的basic认证。
通过UsernamePasswordAuthenticationFilter组装UsernamePasswordAuthenticationToken
去认证。
通过org.springframework.security.web.authentication.www.BasicAuthenticationFilter解析header Authorization
, 然后组装成UsernamePasswordAuthenticationToken去给AuthenticationManager认证。
我们要做的就是模仿UsernamePasswordAuthenticationFilter
或者BasicAuthenticationFilter解析header
将我们的认证凭证传递给AuthenticationManager
.
两种方式我实现了一遍,最终选择了基于UsernamePasswordAuthenticationFilter来实现。
/**
* @author Ryan Miao
* @date 2019/5/30 10:11
* @see org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
*/
public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public TokenAuthenticationFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
boolean debug = this.logger.isDebugEnabled();
String token = TokenUtils.readTokenFromRequest(request);
if (StringUtils.isBlank(token)) {
throw new UsernameNotFoundException("token not found");
}
if (debug) {
this.logger.debug("Token Authentication Authorization header found ");
}
//token包装类, 使用principal来装载token
UsernamePasswordAuthenticationToken tokenAuthenticationToken = new UsernamePasswordAuthenticationToken(
token, null);
//AuthenticationManager 负责解析
Authentication authResult = getAuthenticationManager()
.authenticate(tokenAuthenticationToken);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
return authResult;
}
/**
* 重写认证成功后的方法,不跳转.
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
getRememberMeServices().loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(
new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
chain.doFilter(request, response);
}
}
- TokenUtils来从request里拿到我们的凭证,我这里是从cookie里取出token的值。
- 封装给UsernamePasswordAuthenticationToken的username字段
- 交给getAuthenticationManager()去认证
认证Provider
上一步拿到用户凭证,接下来就是对凭证进行认证。由AuthenticationManager提供。简单理解下AuthenticationManager是什么。
public interface AuthenticationManager {
// ~ Methods
/**
* Attempts to authenticate the passed {@link Authentication} object, returning a
* fully populated <code>Authentication</code> object (including granted authorities)
* if successful.
* <p>
* An <code>AuthenticationManager</code> must honour the following contract concerning
* exceptions:
* <ul>
* <li>A {@link DisabledException} must be thrown if an account is disabled and the
* <code>AuthenticationManager</code> can test for this state.</li>
* <li>A {@link LockedException} must be thrown if an account is locked and the
* <code>AuthenticationManager</code> can test for account locking.</li>
* <li>A {@link BadCredentialsException} must be thrown if incorrect credentials are
* presented. Whilst the above exceptions are optional, an
* <code>AuthenticationManager</code> must <B>always</B> test credentials.</li>
* </ul>
* Exceptions should be tested for and if applicable thrown in the order expressed
* above (i.e. if an account is disabled or locked, the authentication request is
* immediately rejected and the credentials testing process is not performed). This
* prevents credentials being tested against disabled or locked accounts.
*
* @param authentication the authentication request object
*
* @return a fully authenticated object including credentials
*
* @throws AuthenticationException if authentication fails
*/
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
尝试认证传递过来的Authentication对象(即我们的UsernamePasswordAuthenticationToken), 如果认证通过,返回全部信息以及authority权限,否则抛出AuthenticationException异常表示认证失败。
AuthenticationManager的初始化比较复杂,绕了好多路。在我们的SecurityConfig里可以找到声明的地方。
//com.example.serverapi.config.SecurityConfig#authenticationManagerBean
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//com.example.serverapi.config.SecurityConfig#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity)
TokenAuthenticationFilter filter = new TokenAuthenticationFilter("/**");
filter.setAuthenticationManager(authenticationManagerBean());
而AuthenticationManager是AuthenticationManagerDelegator来代替的,其代理的则是org.springframework.security.authentication.ProviderManager。
所以,我们定义provider来认证上一步的token是否合法。
//com.example.serverapi.config.SecurityConfig#configure(org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//DaoAuthenticationConfigurer-DaoAuthenticationProvider用来提供登录时用户名和密码认证
//auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
//自定义TokenAuthenticationProvider, 用来提供token认证
auth.authenticationProvider(new UserTokenAuthenticationProvider());
}
以下是provider全部信息
//com.example.serverapi.domain.security.config.UserTokenAuthenticationProvider
/**
* 这里只使用了username字段。
*
* @author Ryan Miao
* @date 2019/5/29 22:05
*/
public class UserTokenAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
@Override
protected UserDetails retrieveUser(String token,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//验证token
TokenManagement tokenManagement = ServerApiApplication.context
.getBean(TokenManagement.class);
com.example.serverapi.domain.security.entity.User userInfo = tokenManagement.get(token);
if (userInfo == null) {
throw new BadCredentialsException("token认证失败");
}
authentication.setDetails(userInfo);
Set<SimpleGrantedAuthority> authorities = userInfo.getRoleList().stream()
.map(Role::getPermissionList)
.flatMap(Collection::stream)
.map(p -> new SimpleGrantedAuthority(p.getName())).collect(
Collectors.toSet());
return new User(userInfo.getUsername(), userInfo.getPassword(), authorities);
}
/**
* 对应我们的Token令牌类UsernamePasswordAuthenticationToken,可以采用本provide验证.
*/
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
- supports方法来表示本provider提供的认证范围,即传递UsernamePasswordAuthenticationToken的凭证将接受认证
- 自定义了我们自己的Token管理方法TokenManagement,来对token进行认证。根据token拿到userinfo则成功
- 从userInfo里提取authority,创建一个UserDetails,交给下一步的权限校验
权限校验
前面截图里的filter chain,最前面是我们的自定义filter来认证的,最后面的FilterSecurityInterceptor则是权限校验。
//spring-security-core-5.1.4.RELEASE-sources.jar!/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java:229
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
可以看到,调用accessDecisionManager来判断是否继续,权限不足则抛出AccessDeniedException,对应处理就是403了。
AccessDecisionManager目前看到有两种,一个是全局配置,在我们配置Security Config里指定哪些url需要哪些权限。一个是method级别的配置,通过前者校验后判断method是否有权限。
AbstractAccessDecisionManager提供了3种方式。
- AffirmativeBased 任意一种权限校验voter方式通过即通过
- UnanimousBased 必须所有voter通过才可以通过,即任意失败则不通过
- ConsensusBased 通过的voter大于拒绝的voter则通过
- 其他,可以自己实现AbstractAccessDecisionManager
Voter是什么呢?AccessDecisionVoter是真正判断权限的地方。通过对比当前登录用户的authority权限和要访问的资源的权限比较,返回如下code。
- int ACCESS_GRANTED = 1;
- int ACCESS_ABSTAIN = 0;
- int ACCESS_DENIED = -1;
权限移除前缀ROLE_
Spring Security默认使用ROLE_作为authority的前缀,然后表达式里的hasRole, hasAuthority几乎等价,这让我一直很困惑。尤其是当我使用user-role-permission模型的时候,差点以为hasRole是角色判断。所以,为了避免混淆,决定把ROLE_的前缀去掉。
方法就是声明一个类, 具体理由可以追寻源码hasRole来确定。
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
}
统一超级权限admin
到现在差不多已经可以实现用户权限校验了。我们给需要权限敏感的api添加注解,比如@PreAuthorize("hasRole('can_list_user')")
, 然后permission表里添加can_list_user
, 然后角色表role绑定permission,最后把role指派给user。
然而,当系统需要权限的地方特别多的时候,绑定role的代价也很高。比如,我们需要一个超级管理员admin角色,那么这个admin就必须把所有的permission绑定一遍。想想就恐怖。
既然理解了Spring Security的权限校验方式,那么就可以自定义了。我们指定带有admin的authority直接通过,无需校验其他权限。
/**
* 允许设计admin权限的用户直接通过所有认证
* @author Ryan Miao
* @date 2019/6/12 20:49
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();
ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(getExpressionHandler());
decisionVoters
.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
decisionVoters.add(new RoleVoter());
decisionVoters.add(new AuthenticatedVoter());
decisionVoters.add(new AdminVoter());
return new AffirmativeBased(decisionVoters);
}
}
/**
* 拥有admin权限的角色,直接包含所有权限
*
* @author Ryan Miao
* @date 2019/6/12 20:00
*/
public class AdminVoter implements AccessDecisionVoter<Object> {
private static final String ADMIN = "admin";
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
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;
// Attempt to find a matching granted authority
for (GrantedAuthority authority : authorities) {
if (ADMIN.equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
Collection<? extends GrantedAuthority> extractAuthorities(
Authentication authentication) {
return authentication.getAuthorities();
}
@Override
public boolean supports(Class clazz) {
return true;
}
}
总结
初步梳理了Spring Security的认证逻辑和流程,细节的地方还很多,比如SpEL的实现逻辑。但差不多可以理解认证授权是如何实现的了,基于此也足够开展我们的业务开发了。如果说还有想要改造的地方,就是动态权限修改了,为了简化逻辑模型,不做动态权限设定,所有权限初始化指定即可。简单最重要!
使用SpringSecurity搭建授权认证服务(1) -- 基本demo认证原理的更多相关文章
- springboot+spring security +oauth2.0 demo搭建(password模式)(认证授权端与资源服务端分离的形式)
项目security_simple(认证授权项目) 1.新建springboot项目 这儿选择springboot版本我选择的是2.0.6 点击finish后完成项目的创建 2.引入maven依赖 ...
- 使用Spring Cloud Security OAuth2搭建授权服务
阅读数:84139 前言: 本文意在抛砖引玉,帮大家将基本的环境搭起来,具体实战方案还要根据自己的业务需求进行制定.我们最终没有使用Spring Security OAuth2来搭建授权服务,而是完全 ...
- 微信公众号开发系统入门教程(公众号注册、开发环境搭建、access_token管理、Demo实现、natapp外网穿透)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/a1786223749/article/ ...
- springSecurity + jwt + redis 前后端分离用户认证和授权
记录一下使用springSecurity搭建用户认证和授权的代码... 技术栈使用springSecurity + redis + JWT + mybatisPlus 部分代码来自:https://b ...
- SpringBoot使用SpringSecurity搭建基于非对称加密的JWT及前后端分离的搭建
SpringBoot使用SpringSecurity搭建基于非对称加密的JWT及前后端分离的搭建 - lhc0512的博客 - CSDN博客 https://blog.csdn.net/lhc0512 ...
- Nginx + FastCGI 程序(C/C++)搭建高性能web service的demo
http://blog.csdn.net/chdhust/article/details/42645313 Nginx + FastCGI 程序(C/C++)搭建高性能web service的Demo ...
- Spring Cloud2.0之Oauth2环境搭建(授权码模式和密码授权模式)
oauth2 server 微服务授权中心, github源码 https://github.com/spring-cloud/spring-cloud-security 对微服务接口做一些权 ...
- Dubbo入门介绍---搭建一个最简单的Demo框架
Dubbo入门---搭建一个最简单的Demo框架 置顶 2017年04月17日 19:10:44 是Guava不是瓜娃 阅读数:320947 标签: dubbozookeeper 更多 个人分类: D ...
- 大数据江湖之即席查询与分析(下篇)--手把手教你搭建即席查询与分析Demo
上篇小弟分享了几个“即席查询与分析”的典型案例,引起了不少共鸣,好多小伙伴迫不及待地追问我们:说好的“手把手教你搭建即席查询与分析Demo”啥时候能出?说到就得做到,差啥不能差人品,本篇只分享技术干货 ...
随机推荐
- ThreadingTest(线程测试)领先的白框进入这个行业
测试一直是黑色的,白点.在一般情况下,因为白盒测试需要逻辑思维能力是比较高的技术要求比一般开发商的项目经验和谨慎甚至更高,和较长的测试时间,用于单元测试,昂贵的工具,因此,国内企业普遍忽视白盒测试.这 ...
- pip 9.0 离线安装Python3的环境库
到客户现场实施,很多情况下是没有网络的,我们需要在办公室准备好离线安装包. 假设现有已联网的客户机A,一台无网络的客户机B 客户机A 1.生成本地环境的包清单 pip3 freeze > req ...
- ASP .NET Views文件夹下面的文件找不到
习惯将页面和它对应的js,css文件放在一个文件夹下,将这些都放在Views文件夹下 运行的时候发现找不到js和css文件 因为在MVC中,是不建议直接去访问Views文件夹的我们建立的ASP ...
- Qt MVC设计模式(五篇)
http://blog.csdn.net/qq_19672579/article/details/46952675http://blog.csdn.net/qq_19672579/article/de ...
- git 专题
$ git pull origin test // git pull合并代码的时候,若发生冲突,会处于merging状态,检查代码,发现自己的分支低于主分支,这个时候想撤销merge // 撤销mer ...
- NSLocalizedDescription=Request failed: unacceptable content-type: text/html 解决方法
使用AFNetworking请求一个网站出现了以下错误 Error Domain=com.alamofire.error.serialization.response Code=- "Req ...
- 解决WPF中TextBox文件拖放问题
在WPF中,当我们尝试向TextBox中拖放文件,从而获取其路径时,往往无法成功(拖放文字可以成功).造成这种原因关键是WPF的TextBox对拖放事件处理机制的不同,具体可参考这篇文章Textbox ...
- Android零基础入门第74节:Activity启动和关闭
上一期我们学习了Activity的创建和配置,当时留了一个悬念,如何才能在默认启动的Activity中打开其他新建的Activity呢?那么本期一起来学习如何启动和关闭Activity. 一.概述 经 ...
- Setting up multi nodes live migration in Openstack Juno with devstack
Setting up multi nodes live migration in Openstack Juno with devstack Summary Live migration overvie ...
- Spark —— 高可用集群搭建
一.集群规划 这里搭建一个3节点的Spark集群,其中三台主机上均部署Worker服务.同时为了保证高可用,除了在hadoop001上部署主Master服务外,还在hadoop002和hadoop00 ...