springsecurity授权流程,Spring Security 接口认证鉴权入门实践指南
springsecurity授权流程,Spring Security 接口认证鉴权入门实践指南详细介绍
本文目录一览: 如何在SpringBoot Security中实现OAuth2授权
OAuth2已经成为了一个授权的标准协议,大家在很多的产品中都能看到它的身影,比如我们登录一个网站,支持微信,QQ等登录方式,这里QQ和微信提供了授权服务。有很多的授权组件都能提供这种OAuth2认证方式,比如keycloak等,但我们也可以在SpringBoot security中实现OAuth2授权,这篇文章将详细讲解每一个步骤来演示如何在SpringBoot中实现OAuth2授权。
OAuth 2 是一种授权协议,用于通过 HTTP 协议提供对受保护资源的访问。OAuth2 使第三方应用程序能够获得对资源的有限访问。资源的所有者告诉系统,同意授权第三方应用进入系统,获取对这些资源访问。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
由于授权的场景众多,OAuth 2.0 协议定义了获取令牌的四种授权方式,分别是:
四种授权模式分别使用不同的 grant_type 来区分
OAuth 定义了四个角色
访问令牌的职责是在数据过期之前访问数据。
刷新令牌的职责是在现有访问令牌过期时请求新的访问令牌。
要使用 spring security oauth2 模块创建授权服务器,我们需要使用注解@EnableAuthorizationServer 并扩展类 AuthorizationServerConfigurerAdapter。
Spring security oauth 公开了两个用于检查令牌的端点(/oauth/check_token 和 /oauth/token_key),默认它们在 denyAll() 之后受保护。 tokenKeyAccess() 和 checkTokenAccess() 方法打开这些端点以供使用。
ClientDetailsServiceConfigurer 用于定义客户端详细信息,可以基于内存或 JDBC 实现。 为了演示的简单,例子中使用内存实现。 它具有以下重要属性:
clientId –(必需)客户端 ID。
secret(密钥) -(受信任的客户端需要)客户端密钥,如果有的话。
scope – 客户端受限的范围。 如果范围未定义或为空(默认),则客户端不受范围限制。
authorizedGrantTypes – 授权客户端使用的授权类型。 默认值为空。
权限 - 授予客户端的权限(常规 Spring Security 权限)。
redirectUris – 将用户代理重定向到客户端的重定向url。 它必须是绝对 URL。
要创建资源服务器组件,需要使用 @EnableResourceServer 注释并扩展 ResourceServerConfigurerAdapter 类。
上面的配置在 /api 开始的所有端点上启用保护。 所有其他端点都可以自由访问。
资源服务器还提供了一种机制来验证用户本身。 在大多数情况下,它将是基于表单的登录。
上面的 WebSecurityConfigurerAdapter 类设置了一个基于表单的登录页面,并使用 permitAll() 打开授权 URL。
为了简化的目的,仅提供了一个简单的获取用户profile的服务,如下:
我们在浏览器中访问 http://localhost:8080/api/users/me ,会跳转到SpringBoot security提供的登录页面,输入用户名jack,密码123456就可以访问该api,但作为第三方应用程序,没有用户名和密码,只能通过OAuth2令牌来访问
如上面的流程图所示,第一步是从 URL 获取资源所有者的授权,如下的url
它将跳转到Springboot security提供的login页面,用户提供用户名和密码,我们的例子中提供用户名jack,密码123456
上一步获取到了授权码,下一步第三方应用程序将使用授权码来获取访问令牌。 可以使用curl命令来获取access token。
authorization中需要使用basic认证,提供用户名clientapp,密码123456,可以使用下面的网站来获取加密后的字符串
授权服务将返回如下的信息,包括access_token,refresh_token和token_type等。
获得访问令牌后,我们可以前往资源服务器获取受保护的API.使用curl命令来访问API,authorization是上一步获取到的access_token。
获取信息如下:
SpringSecurity认证流程分析
要想分析SpringSecurity的认证流程,就一定要先了解整个SpringSecurity的工作流程,我们才能最终进行一些自定义操作。
Spring Security的web基础是Filters(过滤器) ,它是通过一层层的Filters来对web请求做处理的,每一个web请求会经过一条过滤器链,在这个过程中完成认证与授权。
其具体工作流程是这样的:
有了以上的一些基础了解后,我们来顺着源码流程走一边,理清整个认证的流程。
基于formLogin的流程分析,SpringSecurity默认也是formLogin。
以下源码我都将注释去掉,否则太长了!
【项目实践】一文带你搞定前后端分离下的认证和授权|Spring Security + JWT
SpringSecurity+JWT认证流程解析 | 掘金新人第一弹
SpringSecurit(小胖哥)
3.spring security oauth2 配置授权服务器(AuthorizationServerConfigurerAdapter)
项目地址: https://github.com/liangjinquan17/spring-security-demo
配置AuthorizationServerConfigurerAdapter需要注意的地方有三,1.密码校验方式 (PasswordEncoder)2.AuthenticationManager 3.UserDetailsService
1.新建类继承AuthorizationServerConfigurerAdapter,重写三个config方法,注入PasswordEncoder、AuthenticationManager、UserDetailsService。这三个实现类均有WebSecurityConfigurerAdapter类提供。
2.首先我们来看下WebSecurityConfigurerAdapter类是怎样提供PasswordEncoder、AuthenticationManager、UserDetailsService的。
3.接着我们来看下继承AuthorizationServerConfigurerAdapter的新类
4.调用授权接口获取token值:
http://localhost:8080/oauth/token?username=admin&password=admin&grant_type=password&scope=admin
注意了,我们不是配置了client和secret的,这时候需要把这两个参数带上,可以放header也可以放body里面;
5.至于oauth2的四种授权方式我就不一一配置了。
Spring Security源码(一):认证、授权、过滤器链
因为看了很多博客,发现很多人的使用风格都不一样,有点懵。感觉最好的方法就是多看一些源码,然后自己选择想要的使用方式。 spring security版本:2.1.6.RELEASE
这里先讲这三种的关系 AuthenticationManager、ProviderManager、AuthenticationProvider
封装了用户身份信息
一个接口,只有一个方法
ProviderManager是AuthenticationManager的实现类,提供了基本认证实现逻辑和流程; 先看这个方法这个方法,是 用来认证 的方法
关键点
AuthenticationProvider 本身也就是一个 接口 ,它有实现类 AbstractUserDetailsAuthenticationProvider 和AbstractUserDetailsAuthenticationProvider的 子类DaoAuthenticationProvider
只有 两个 方法
总结 这是一个抽象类,是 模板模 式。定义好了认证流程,我们去实现流程中的一些 环节 方法。达到了 自定义认证 的效果
AbstractUserDetailsAuthenticationProvider 的子类,实现了一些 抽象方法
retrieveUser
主要就是用 UserDetailsService 去查找用户
additionalAuthenticationChecks
createSuccessAuthentication
本质还是调用了父类的方法,就是新建了一个Authentication,然后将 UserDetails 的 信息 传递过去
UserDetailsService是一个接口,提供了一个方法
感觉这个接口没什么用,就是用来管理用户类的
SS在http后台中起作用主要是基于 Servlet Filters 的,我们先来看看什么是 Filter 是如何作用在 Servlet 中的。
springSecurityFilterChain 是个接口 , DefaultSecurityFilterChain 是它的实现类,而DefaultSecurityFilterChain 内部存在这一个 Filters 列表 ,关于SS中的过滤器和他们的执行顺序(Order)可以查看 官方文档 ,当我们需要自定义Filter的时候就会用到。 当请求到来时,在 ss 里边的 Filter就会作用请求,如下图 :
在上面我们说到了SS有 自己的一条过滤器链 ,下面就是截图:(执行顺序就是集合中的顺序)
下面说一下几个 比较重要的 Filter 的 处理逻辑
整个调用流程是,先调用其父类 AbstractAuthenticationProcessingFilter.doFilter() 方法,然后再执行 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法进行验证;
父类是 AbstractAuthenticationProcessingFilter (又是模板模式,子类实现抽象方法,父类定好流程)
requiresAuthentication 判断是否需要当前filter处理,就是判断路径、请求方法等等
以UsernamePasswordAuthenticationFilter的构造方法为例
successfulAuthentication
这里的认证就是 第一节的内容 ( this.getAuthenticationManager().authenticate(authRequest) )
从上图中过滤器的执行顺序图中可以看出 AnonymousAuthenticationFilter 过滤器是在UsernamePasswordAuthenticationFilter 等过滤器之后 ,如果它前面的过滤器 都没有认证成功 ,Spring Security则为当前的SecurityContextHolder中添加一个Authenticaiton 的匿名实现类 AnonymousAuthenticationToken;
总结
ExceptionTranslationFilter 异常处理过滤器,该过滤器用来处理在系统 证授权过程 中抛出的异常(也就是下一个过滤器FilterSecurityInterceptor),主要是 处理 AuthenticationException 和 AccessDeniedException 。
此过滤器为认证授权过滤器链中 最后一个过滤器 ,该过滤器之后就是请求真正的 /xx服务
Spring Security默认使用 AffirmativeBased 实现 AccessDecisionManager 的 decide 方法来实现授权
到此位置 authentication 当前用户信息,fl当前访问的 资源路径及attributes当前资源路径的决策 (即是否需要认证)。剩下就是 判断 当前用户的角色Authentication.authorites 是否权限 访问决策访问当前资源fi
Spring Security 核心过滤器链分析
Spring security OAuth2 深入解析
话不多说,先上图:
分析一波:
其实不管微信或者QQ大体上都是使用这种OAuth2的基本流程:
OAuth2 在服务提供者上可分为两类:
注:这两者有时候可能存在同一个应用程序中(即SOA架构)。在Spring OAuth中可以简便的将其分配到两个应用中(即微服务),而且可多个资源获取服务共享一个授权认证服务。
主要的操作:
分析一波: 1) 第一步操作 :
注:其中 client_id和 client_secret都是授权服务器发送给第三方应用的,如:微信等一系列授权,在其平台上注册,获取其appid和secret同样道理(个人理解为账号密码)。
既然是账号秘密,总不能以get请求,也太不安全了。因此,OAuth2要求该请求必须是POST请求,同时,还必须时HTTPS服务,以此保证获取到的安全凭证(Access Token)的安全性。
2) 第二步操作 :
3) 第三步操作 :
主要的操作:
spring OAuth2中,我们配置一个授权认证服务,我们最主要有以下三点:
spring中有三个配置与这三点一一对应:
除了上面说到的 client_id和 client_secret,还需要一些服务附带一些授权认证参数。
1). Grant Type
其实OAuth2不仅提供授权码(code)这种格式授权方式,还提供几个其他类型。其中用Grant Type代表当前授权的类型。 Grant Type包括:
2). scope
其实授权赋予第三方用户可以在资源服务器获取资源,经常就是调取Api请求附带令牌,然而调取api有增删查改等功能,而scopes的值就是all(全部权限),read,write等权限。就是第三方访问资源的一个权限,访问范围。
3). accessTokenValiditySeconds
还可以设置accessTokenValiditySeconds属性来设置Access Token的存活时间。
AccessToken的存在意义:
1). AuthorizationServerTokenServices AuthorizationServerTokenServices 提供了对AccessToken的相关操作创建、刷新、获取。
2). DefaultTokenServices
AuthorizationServerTokenServices竟然可以操作AccessToken,那么OAuth2就默认为我们提供了一个默认的DefaultTokenServices。包含了一些有用实现,可以使用它来修改令牌的格式和令牌的存储等,但是生成的token是随机数。
3). TokenStore
创建AccessToken完之后,除了发放给第三方,肯定还得保存起来,才可以使用。因此,TokenStore为我们完成这一操作,将令牌(AccessToken)保存或持久化。 TokenStore也有一个默认的实现类InMemoryTokenStore,从名字就知道是通过保存到内存进而实现保存Access Token。 TokenStore的实现有多种类型,可以根据业务需求更改Access Token的保存类型:
4). JWT Token
想使用jwt令牌,需要在授权服务中配置JwtTokenStore。之前说了,jwt将一些信息数据编码后存放在令牌,那么其实在传输的时候是很不安全的,所以Spring OAuth2提供了JwtAccessTokenConverter来怼令牌进行编码和解码。适用JwtAccessTokenConverter可以自定义秘签(SigningKey)。SigningKey用处就是在授权认证服务器生成进行签名编码,在资源获取服务器根据SigningKey解码校验。
授权认证是使用AuthorizationEndpoint这个端点来进行控制,一般使用AuthorizationServerEndpointsConfigurer 来进行配置。
1).端点(endpoints)的相关属性配置:
2).端点(endpoints)的授权url: 要授权认证,肯定得由url请求,才可以传输。因此OAuth2提供了配置授权端点的URL。 AuthorizationServerEndpointsConfigurer ,还是这个配置对象进行配置,其中由一个pathMapping()方法进行配置授权端点URL路径,默认提供了两个参数defaultPath和customPath:
pathMapping的defaultPath有:
注:pathMapping的两个参数都将以 "/" 字符为开始的字符串
实际上我们上面说到的端点,其实可以看成Controller,用于返回不同端点的响应内容。
授权服务的错误信息是使用标准的Spring MVC来进行处理的,也就是 @ExceptionHandler 注解的端点方法,我们可以提供一个 WebResponseExceptionTranslator 对象。最好的方式是改变响应的内容而不是直接进行渲染。
资源服务器,其实就是存放一些受令牌保护的资源,只有令牌并且有效正确才能获取到资源。 内部是通过Spring OAuth2的Spring Security Authentication filter 的过滤链来进行保护。
我们可以继承ResourceServerConfigurerAdapter,来使用 ResourceServerSecurityConfigurer进行相关配置。
ResourceServerTokenServices 是组成授权服务的另一半。
1).若是资源服务器和授权服务在同一个应用,可以使用DefaultTokenServices
2).若是分离的。ResourceServerTokenServices必须知道令牌的如何解码。
ResourceServerTokenServices解析令牌的方法:
注:授权认证服务需要把/oauth/check_toke暴露出来,并且附带上权限访问。
(~ ̄▽ ̄)~未完待续... ...
Sercurity + Auth2框架实现认证授权
SpringSecurity 是Spring家族的一员,是Spring家族提供的安全框架,提供认证和授权功能,最主要的是它提供了简单的使用方式,同时又有很高的灵活性,简单,灵活,强大。
主要的主要的几个配置类
自定义Security的策略
AuthorizationServerConfigurerAdapter
例子如下:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
? ? public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
? ? ? ? endpoints
? ? ? ? ? ? ? ? .authenticationManager(authenticationManager)
? ? ? ? ? ? ? ? .tokenGranter(new CompositeTokenGranter(getTokenGranters(endpoints)))
? ? ? ? ? ? ? ? .userDetailsService(userDetailsService)
? ? ? ? ? ? ? ? .tokenStore(tokenStore());
}
@Override
? ? public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
? ? ? ? //存放到db
? ? ? ? clients.jdbc(dataSource);
//? ? ? ? 存放到内存
? ? ? ? clients.inMemory()
? ? ? ? ? ? ? ? .withClient("webapp")
? ? ? ? ? ? ? ? .secret(pass)
? ? ? ? ? ? ? ? .scopes("server")
? ? ? ? ? ? ? ? .authorizedGrantTypes("password", "authorization_code", "refresh_token")
? ? ? ? ? ? ? ? .accessTokenValiditySeconds(24 * 60 * 60) //token有效期
? ? ? ? ? ? ? ? .refreshTokenValiditySeconds(24 * 60 * 60)
? ? ? ? ? ? ? ? .and().withClient("app")
? ? ? ? ? ? ? ? .secret(pass)
? ? ? ? ? ? ? ? .scopes("app")
? ? ? ? ? ? ? ? .authorizedGrantTypes("authorization_code", "refresh_token")
? ? ? ? ? ? ? ? .redirectUris("https://www.baidu.com/");
? ? }
/**
? ? * 填充认证方式
? ? * @param endpoints endpoints
? ? * @return list
? ? */
? ? private List
getTokenGranters(AuthorizationServerEndpointsConfigurer endpoints) {
? ? ? ? AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
? ? ? ? ClientDetailsService clientDetailsService = endpoints.getClientDetailsService();
? ? ? ? OAuth2RequestFactory oAuth2RequestFactory = endpoints.getOAuth2RequestFactory();
? ? ? ? AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
? ? ? ? return new ArrayList<>(Arrays.asList(
? ? ? ? ? ? ? ? new RefreshTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory),
? ? ? ? ? ? ? ? new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService,
? ? ? ? ? ? ? ? ? ? ? ? oAuth2RequestFactory),
? ? ? ? ? ? ? ? new ResourceOwnerPasswordTokenGranter(authenticationManager, endpoints.getTokenServices(),
? ? ? ? ? ? ? ? ? ? ? ? clientDetailsService, oAuth2RequestFactory),
? ? ? ? ? ? ? ? new MyAbstractTokenGranter(authenticationManager, tokenServices, clientDetailsService,
? ? ? ? ? ? ? ? ? ? ? ? oAuth2RequestFactory)));
? ? }
}
默认情况下spring?security?oauth?的http配置。
ResourceServerConfigurerAdapter:
例子如下
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
? ? @Override
? ? public void configure(HttpSecurity http) throws Exception {
? ? ? ? http
? ? ? ? ? ? ? ? .csrf().disable()
? ? ? ? ? ? ? ? .exceptionHandling()
? ? ? ? ? ? ? ? .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
? ? ? ? ? ? ? ? .and()
? ? ? ? ? ? ? ? .authorizeRequests()
? ? ? ? ? ? ? ? .antMatchers("/index/getUser").permitAll()
? ? ? ? ? ? ? ? .antMatchers("/user/getUserPassword").permitAll()
? ? ? ? ? ? ? ? .anyRequest().authenticated()
? ? ? ? ? ? ? ? .and()
? ? ? ? ? ? ? ? .httpBasic();
? ? }
}
WebSecurityConfigurerAdapter 类是个适配器, 需要配置的时候需要我们自己写个配置类去继承,根据自己的需求去进行配置即可WebSecurityConfigurerAdapter的配置拦截要优先于ResourceServerConfigurerAdapter,优先级高的http配置是可以覆盖优先级低的配置的。
WebSecurityConfigurerAdapter
例子如下:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
? ? /**
? ? * 自定义实现用户信息获取
? ? *
? ? * @return
? ? */
? ? @Bean
? ? @Override
? ? public UserDetailsService userDetailsService() {
? ? ? ? return new MyUserDetailsServiceImpl();
? ? }
? ? /**
? ? * @param auth
? ? * @throws Exception
? ? */
? ? @Override
? ? protected void configure(AuthenticationManagerBuilder auth) throws Exception {
? ? ? ? auth.userDetailsService(userDetailsService())
? ? ? ? ? ? ? ? .passwordEncoder(passwordEncoder());
? ? }
? ? /**
? ? * @param http
? ? * @throws Exception
? ? */
? ? @Override
? ? protected void configure(HttpSecurity http) throws Exception {
? ? ? ? http.csrf().disable()
? ? ? ? ? ? ? ? .authorizeRequests().anyRequest().authenticated()
? ? ? ? ? ? ? ? .and()
? ? ? ? ? ? ? ? .httpBasic();
? ? }
? ? /**
? ? * 不定义没有password grant_type
? ? */
? ? @Override
? ? @Bean
? ? public AuthenticationManager authenticationManagerBean() throws Exception {
? ? ? ? return super.authenticationManagerBean();
? ? }
? ? /**
? ? * 密码解析器
? ? * @return PasswordEncoder
? ? */
? ? @Bean
? ? public PasswordEncoder passwordEncoder() {
? ? ? ? return new BCryptPasswordEncoder();
? ? }
}
继承了抽象类 AbstractTokenGranter
AbstractTokenGranter
public class MyAbstractTokenGranter extends AbstractTokenGranter {
? ? @Autowired
? ? private UserInfo userInfo;
? ? @Autowired
? ? private RedisUtil redisUtil;
? ? private static final String GRANT_TYPE = "sms_cod";
? ? private final AuthenticationManager authenticationManager;
? ? public MyAbstractTokenGranter(AuthenticationManager authenticationManager,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? AuthorizationServerTokenServices tokenServices,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ClientDetailsService clientDetailsService,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OAuth2RequestFactory requestFactory) {
? ? ? ? this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
? ? }
? ? protected MyAbstractTokenGranter(AuthenticationManager authenticationManager,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? AuthorizationServerTokenServices tokenServices,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ClientDetailsService clientDetailsService,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OAuth2RequestFactory requestFactory, String grantType) {
? ? ? ? super(tokenServices, clientDetailsService, requestFactory, grantType);
? ? ? ? this.authenticationManager = authenticationManager;
? ? }
? ? /**
? ? * 我们拿到的 token 终会过期的, 对应于刷新 token模式的 RefreshTokenGranter 则负责获取新的 OAuth2AccessToken。
? ? *
? ? * @param client
? ? * @param tokenRequest
? ? * @return
? ? */
? ? @Override
? ? protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
? ? ? ? // 传入的参数需要有 refresh_token (DefaultOAuth2AccessToken 中有 refreshToken 字段)
? ? ? ? String refreshToken = tokenRequest.getRequestParameters().get("refresh_token");
? ? ? ? // 调用 tokenService 的刷新方法得到新的 OAuth2AccessToken
? ? ? ? return getTokenServices().refreshAccessToken(refreshToken, tokenRequest);
? ? }
? ? /**
? ? * 用户拦截
? ? *
? ? * @param client? ? ? client
? ? * @param tokenRequest 请求的令牌
? ? * @return
? ? */
? ? @Override
? ? protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
? ? ? ? Map
parameters = new LinkedHashMap
(tokenRequest.getRequestParameters());
? ? ? ? String username = parameters.get("username");
? ? ? ? String password = parameters.get("password");
? ? ? ? String mobile = parameters.get("mobile");
? ? ? ? //客户端提交的验证码
? ? ? ? String smscode = parameters.get("sms_cod");
? ? ? ? String type = parameters.get("type");
? ? ? ? parameters.remove("password");
? ? ? ? //验证用户状态(是否警用等)
//? ? ? ? UserInfo userInfo = new UserInfo();
//? ? ? ? userInfo.setMobile(mobile);
? ? ? ? // 验证验证码
? ? ? ? //获取服务中保存的用户验证码
? ? ? ? String smsCheckCode = (String) redisUtil.get(userInfo.getMobile());
? ? ? ? if (StringUtils.isBlank(smsCheckCode)) {
? ? ? ? ? ? throw new InvalidGrantException("用户没有发送验证码");
? ? ? ? }
? ? ? ? if (!smscode.equals(smsCheckCode)) {
? ? ? ? ? ? throw new InvalidGrantException("验证码不正确");
? ? ? ? } else {
? ? ? ? ? ? //验证通过后从缓存中移除验证码
? ? ? ? ? ? redisUtil.setRemove(userInfo.getMobile(), smsCheckCode);
? ? ? ? }
? ? ? ? if (username.isEmpty()) {
? ? ? ? ? ? throw new InvalidGrantException("用户不存在");
? ? ? ? }
? ? ? ? Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
? ? ? ? ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
? ? ? ? try {
? ? ? ? ? ? userAuth = authenticationManager.authenticate(userAuth);
? ? ? ? } catch (AccountStatusException ase) {
? ? ? ? ? ? //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
? ? ? ? ? ? throw new InvalidGrantException(ase.getMessage());
? ? ? ? } catch (BadCredentialsException e) {
? ? ? ? ? ? // If the username/password are wrong the spec says we should send 400/invalid grant
? ? ? ? ? ? throw new InvalidGrantException(e.getMessage());
? ? ? ? }
? ? ? ? if (userAuth == null || !userAuth.isAuthenticated()) {
? ? ? ? ? ? throw new InvalidGrantException("Could not authenticate user: " + username);
? ? ? ? }
? ? ? ? OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
? ? ? ? return new OAuth2Authentication(storedOAuth2Request, userAuth);
? ? }
核心依赖配置配置如下:
? ? ? ? ? ?
org.springframework.security.oauth
? ? ? ? ? ?
spring-security-oauth2
? ? ? ? ? ?
${oauth2.version}
? ? ? ?
? ? ? ?
? ? ? ? ? ?
org.springframework.cloud
? ? ? ? ? ?
spring-cloud-starter-security
? ? ? ? ? ?
? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ?
spring-security-oauth2
? ? ? ? ? ? ? ? ? ?
org.springframework.security.oauth
? ? ? ? ? ? ? ?
? ? ? ? ? ?
使用SpringSrcurity+Anut2根据token实现用户密码登录
service层
/**
? ? * 根据用户名查询
? ? * @param userName 用户名
? ? * @return 返回的路径
? ? */
? ? UserInfo getUserInfoByName(@Param("userName") String userName);
Service层
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
? ? @Autowired
? ? public PasswordEncoder passwordEncoder;
? ? @Resource
? ? private UserInfoService userInfoService;
? ? @Resource
? ? private UserInfoMapper userInfoMapper;
? ? @Override
? ? public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
? ? ? ? UserInfo userInfo = userInfoService.getUserInfoByName(username);
? ? ? ? if (userInfo == null){
? ? ? ? ? ? throw new UsernameNotFoundException(username);
? ? ? ? }
? ? ? ? return userInfo;
? ? }
Controlleer层
@RestController
@RequestMapping("/")
public class IndexController {
? ? /**
? ? * user
? ? * @param user user
? ? * @return Principal
? ? */
? ? @GetMapping(value = "/user")
? ? public Principal user(Principal user) {
? ? ? ? return user;
? ? }
Spring Security对Android客户端如何实现权限管理
后台的东西无论是安卓,ios都差不多的,如果后台用的是Spring+spring mvc +hibernate架构的话,先导入spring security的包进项目,然后编写spring security配置文件(百度上一大堆),如果对安全要求不高,直接全部用默认的类就能实施一个安全登录的东西了,如果有其他要求,就需要自己编写一些类比如过滤器之类的,然后作为一个普通bean使用。记得要在web.xml中包含你的配置文件,若全部使用默认配置,数据库那里的表名、字段名,表单的action属性,账号、密码的name属性都有要求(是一个定值),要注意。全部都是本人在使用spring security时候的一点小经验,望采纳
Spring Cloud Security[微服务安全](一)初识Spring Cloud Security和OAuth2.0
在介绍Spring Cloud Security之前,我们先要介绍一下Spring Security。
Spring Security是一套提供了完整的声明式安全访问控制的解决方案。 Spring Security是基于Spring AOP和Servlet过滤器的,它只能服务基于Spring的应用程序。 除常规认证和授权外,它还提供 ACL,LDAP,JAAS,CAS 等高级安全特性以满足复杂环境中的安全需求。
(注:在Spring Security中,Authority和Permission是两个完全独立的概念,两者没有必然的联系。它们之前需要通过配置进行关联。)
安全方案的实现通常分为 认证(Authentication) 和 授权(Authorization) 两部分。
使用者可以使一个 用户 , 设备 ,和可以在应用程序中执行某种操作的 其它系统 。
认证一般通过用户名和密码的正确性校验来完成通过或拒绝。
Spring Security支持的主流认证如下:
Spring Security进行认证的步骤: ++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++ 1. 用户使用用户名和密码登陆 ++ ++ 2. 过滤器(UsernamePasswordAuthenticationFilter)获取到用户名 ++ ???和密码,将它们封装成Authentication ++ ++ 3. AuthenticationManager认证Token(由Authentication实现类传递) ++ ++ 4. AuthenticationManager认证成功,返回一个封装了用户权限信息 ++ ???的Authentication对象,包含用户的上下文信息(角色列表) ++ ++ 5. 将Authentication对象赋值给当前的SecurityContext,建立这个用户 ++ 的安全上下文( SecurityContextHolder.getContext.setAuthentication() ) ++ ++ 6. 当用户进行一些受到访问控制机制保护的操作,访问控制机制会依据 ++ ???当前安全的上下文信息来检查这个操作所需的权限 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
Spring Security提供了一系列基本组件,如spring-security-acl, spring-security-cas, spring-security-oauth2等。 具体这里就不详述了,读者可以查看官网的介绍。
?
Spring Cloud Security是用于构建微服务系统架构下的安全的应用程序和服务,它可以轻松实现基于微服务架构的统一的安全认证与授权。
Spring Cloud Security相对于Spring Security整合了Zuul,Feign,而且更加完美地整合了OAuth2.0。
?
OAuth 2.0是用于授权的行业标准协议。
原理: OAuth2是用户资源和第三方应用之间的一个中间层。 它把资源和第三方应用隔开,使得第三方应用无法直接访问资源,第三方应用要访问资源需要通过提供 凭证(Token) 来获得OAuth 2.0授权。
OAuth2的典型例子:
============================================== == 微信公众号授权提醒 == 页面弹出一个提示框需要获取我们的个人信息 == 单击确定 == 第三方应用会获取我们在微信公众平台中的个人信息 ==============================================
OAuth的关键角色:
在用户授权第三方获取私有资源后,第三方通过获取到的token等信息通过授权服务器认证,然后去资源服务器获取资源。
?
密码模式中,资源拥有者负责向客户端提供用户名和密码; 客户端根据用户名和密码箱认证服务器申请令牌,正确后认证服务器发放令牌。
客户端请求参数:
缺点: 用户对客户端高度可信,必须把自己的密码发给客户端,存在被黑客窃取的隐患(不推荐)。
在授权码模式中,客户端是通过其后台服务器与认证服务器进行交互的。
授权码模式的运行流程:
从以上图中我们可以看到客户端服务器和认证服务器需经过2次握手,客户端服务器才能拿到最终的访问和更新令牌。
客户端申请认证的参数:
?
服务器返回的参数:
?
客户端收到授权码以后,附上重定向URI,向认证服务器申请访问令牌。
客户端申请令牌的参数:
?
服务器返回的参数:
?
如果访问令牌已经过期,可以使用更新令牌申请一个新的访问令牌。
通过更新令牌申请访问令牌的参数:
这两种模式不常用,因此在这里就不多叙述了。
SpringSecurity+JWT认证流程解析
本文适合: 对Spring Security有一点了解或者跑过简单demo但是对整体运行流程不明白的同学,对SpringSecurity有兴趣的也可以当作你们的入门教程,示例代码中也有很多注释。 大家在做系统的时候,一般做的第一个模块就是 认证与授权 模块,因为这是一个系统的入口,也是一个系统最重要最基础的一环,在认证与授权服务设计搭建好了之后,剩下的模块才得以安全访问。 市面上一般做认证授权的框架就是shiro和Spring Security,也有大部分公司选择自己研制。出于之前看过很多Spring Security的入门教程,但都觉得讲的不是太好,所以我这两天在自己鼓捣Spring Security的时候萌生了分享一下的想法,希望可以帮助到有兴趣的人。
Spring Security框架我们主要用它就是解决一个认证授权功能,所以我的文章主要会分为两部分:
我会为大家用一个Spring Security + JWT + 缓存的一个demo来展现我要讲的东西,毕竟脑子的东西要体现在具体事物上才可以更直观的让大家去了解去认识。 学习一件新事物的时候,我推荐使用自顶向下的学习方法,这样可以更好的认识新事物,而不是盲人摸象。
注 :只涉及到用户认证授权不涉及oauth2之类的第三方授权。
想上手 Spring Security 一定要先了解它的工作流程,因为它不像工具包一样,拿来即用,必须要对它有一定的了解,再根据它的用法进行自定义操作。
我们可以先来看看它的工作流程: 在Spring Security的官方文档上有这么一句话:
Spring Security 的web基础是Filters。
这句话展示了Spring Security的设计思想: 即通过一层层的Filters来对web请求做处理。
放到真实的Spring Security中,用文字表述的话可以这样说:
一个web请求会经过一条过滤器链,在经过过滤器链的过程中会完成认证与授权,如果中间发现这条请求未认证或者未授权,会根据被保护API的权限去抛出异常,然后由异常处理器去处理这些异常。
用图片表述的话可以这样画,这是我在百度找到的一张图片:
如上图,一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是我们本篇主要讲的负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。
图中的这两个绿色过滤器我们今天不会去说,因为这是Spring Security对form表单认证和Basic认证内置的两个Filter,而我们的demo是JWT认证方式所以用不上。
如果你用过Spring Security就应该知道配置中有两个叫formLogin和httpBasic的配置项,在配置中打开了它俩就对应着打开了上面的过滤器。
换言之,你配置了这两种认证方式,过滤器链中才会加入它们,否则它们是不会被加到过滤器链中去的。
因为Spring Security自带的过滤器中是没有针对JWT这种认证方式的,所以我们的demo中会 写一个JWT的认证过滤器,然后放在绿色的位置进行认证工作。
知道了Spring Security的大致工作流程之后,我们还需要知道一些非常重要的概念也可以说是组件:
上下文对象,认证后的数据就放在这里面,接口定义如下:
这个接口里面只有两个方法,其主要作用就是get or set Authentication。
可以说是SecurityContext的工具类,用于get or set or clear SecurityContext,默认会把数据都存储到当前线程中。
这几个方法效果如下:
Authentication只是定义了一种在SpringSecurity进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。
AuthenticationManager定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,默认使用的实现类为:ProviderManager。 接下来大家可以构思一下如何将这四个部分,串联起来,构成Spring Security进行认证的流程: 1. 先是一个请求带着身份信息进来 2. 经过AuthenticationManager的认证, 3. 再通过SecurityContextHolder获取SecurityContext, 4. 最后将认证后的信息放入到SecurityContext。
真正开始讲诉我们的认证代码之前,我们首先需要导入必要的依赖,数据库相关的依赖可以自行选择什么JDBC框架,我这里用的是国人二次开发的myabtis-plus。
接着,我们需要定义几个必须的组件。 由于我用的Spring-Boot是2.X所以必须要我们自己定义一个加密器:
这个Bean是不必可少的,Spring Security在认证操作时会使用我们定义的这个加密器,如果没有则会出现异常。
实现UserDetailsService的抽象方法并返回一个 UserDetails 对象,认证过程中SpringSecurity会调用这个方法访问数据库进行对用户的搜索,逻辑什么都可以自定义,无论是从数据库中还是从缓存中,但是我们需要将我们查询出来的用户信息和权限信息组装成一个 UserDetails 返回。
UserDetails 也是一个定义了数据形式的接口,用于保存我们从数据库中查出来的数据,其功能主要是验证账号状态和获取权限,具体实现可以查阅我仓库的代码。
由于我们是JWT的认证模式,所以我们也需要一个帮我们操作Token的工具类,一般来说它具有以下三个方法就够了:
在下文我的代码里面,JwtProvider充当了Token工具类的角色,具体实现可以查阅我仓库的代码。
有了前面的讲解之后,大家应该都知道用SpringSecurity做JWT认证需要我们自己写一个过滤器来做JWT的校验,然后将这个过滤器放到绿色部分。 在我们编写这个过滤器之前,我们还需要进行一个认证操作,因为我们要先访问认证接口拿到token,才能把token放到请求头上,进行接下来请求。 如果你不太明白,不要紧,先接着往下看我会在这节结束再次梳理一下。
访问一个系统,一般最先访问的是认证方法,这里我写了最简略的认证需要的几个步骤,因为实际系统中我们还要写登录记录啊,前台密码解密啊这些操作。
这里一共五个步骤,大概只有前四步是比较陌生的:
这样的话就算完成了,感觉上很简单,因为主要认证操作都会由authenticationManager.authenticate()帮我们完成。
接下来我们可以看看源码,从中窥得Spring Security是如何帮我们做这个认证的(省略了一部分):
看了源码之后你会发现和我们平常写的一样,其主要逻辑也是查数据库然后对比密码。 登录之后效果如下:
我们返回token之后,下次请求其他API的时候就要在请求头中带上这个token,都按照JWT的标准来做就可以。
有了token之后,我们要把过滤器放在过滤器链中,用于解析token,因为我们没有session,所以我们每次去辨别这是哪个用户的请求的时候,都是根据请求中的token来解析出来当前是哪个用户。 所以我们需要一个过滤器去拦截所有请求,前文我们也说过,这个过滤器我们会放在绿色部分用来替代UsernamePasswordAuthenticationFilter,所以我们新建一个JwtAuthenticationTokenFilter,然后将它注册为Bean,并在编写配置文件的时候需要加上这个:
addFilterBefore的语义是添加一个Filter到XXXFilter之前,放在这里就是把JwtAuthenticationTokenFilter放在UsernamePasswordAuthenticationFilter之前,因为filter的执行也是有顺序的,我们必须要把我们的filter放在过滤器链中绿色的部分才会起到自动认证的效果。 接下来我们可以看看JwtAuthenticationTokenFilter的具体实现了:
代码里步骤虽然说的很详细了,但是可能因为代码过长不利于阅读,我还是简单说说,也可以直接去仓库查看源码:
这样的话,每一个带有正确token的请求进来之后,都会找到它的账号信息,并放在上下文对象中,我们可以使用SecurityContextHolder很方便的拿到上下文对象中的Authentication对象。
完成之后,启动我们的demo,可以看到过滤器链中有以下过滤器,其中我们自定义的是第5个:
就酱,我们登录完了之后获取到的账号信息与角色信息我们都会放到缓存中,当带着token的请求来到时,我们就把它从缓存中拿出来,再次放到上下文对象中去。
结合认证方法,我们的逻辑链就变成了:
登录拿到token请求带上tokenJWT过滤器拦截校验token将从缓存中查出来的对象放到上下文中
这样之后,我们认证的逻辑就算完成了。
认证和JWT过滤器完成后,这个JWT的项目其实就可以跑起来了,可以实现我们想要的效果,如果想让程序更健壮,我们还需要再加一些辅助功能,让代码更友好。
当用户未登录或者token解析失败时会触发这个处理器,返回一个非法访问的结果。
当用户本身权限不满足所访问API需要的权限时,触发这个处理器,返回一个权限不足的结果。
用户退出一般就是清除掉上下文对象和缓存就行了,你也可以做一下附加操作,这两步是必须的。
JWT的项目token刷新也是必不可少的,这里刷新token的主要方法放在了token工具类里面,刷新完了把缓存重载一遍就行了,因为缓存是有有效期的,重新put可以重置失效时间。
这篇文我从上周日就开始构思了,为了能讲的老妪能解,修修改改了几遍才发出来。
作者:和耳朵 链接:https://juejin.cn/post/6846687598442708999
Spring Security 接口认证鉴权入门实践指南
Web API 接口服务场景里,用户的认证和鉴权是很常见的需求,Spring Security 据说是这个领域里事实上的标准,实践下来整体设计上确实有不少可圈可点之处,也在一定程度上印证了小伙们经常提到的 “太复杂了” 的说法也是很有道理的。
本文以一个简单的 SpringBoot Web 以应用为例,重点介绍以下内容:
创建 SpringBoot 示例,用于演示 Spring Security 在 SpringBoot 环境下的应用,简要介绍四部分内容:pom.xml、application.yml、IndexController 和 HelloController。
boot-example 是用于演示的 SpringBoot 项目子模块(Module)。
SpringBoot 应用名称为 example,实例端口为 9999 。
IndexController 实现一个接口:/。
HelloController 实现两个接口:/hello/world 和 /hello/name。
编译启动 SpringBoot 应用,通过浏览器请求接口,请求路径和响应结果:
SpringBoot 示例准备完成。
SpringBoot 集成 Spring Security 仅需要在 pom.xml 中添加相应的依赖: spring-boot-starter-security ,如下:
编译启动应用,相对于普通的 SpringBoot 应用,我们可以在命令行终端看到 特别 的两行日志:
表示 Spring Security 已在 SpringBoot 应用中生效。 默认 情况下,Spring Security 自动化 地帮助我们完成以下三件事件:
使用 Spring Security 默认为我们生成的用户名和密码进行登录(Sign in),成功之后会自动重定向至 / :
之后我们就可以通过浏览器正常请求 /hello/world 和 /hello/name。
默认情况下,Spring Security 仅支持基于 FormLogin 方式的认证,只能使用固定的用户名和随机生成的密码,且不支持鉴权。如果想要使用更丰富的安全特性:
本文以 Java Configuration 的方式为例进行介绍,需要我们提供一个继承自 WebSecurityConfigurerAdapter 配置类,然后通过重写若干方法进而实现自定义配置。
SecurityConfig 使用 @Configuration 注解(配置类),继承自 WebSecurityConfigurerAdapter ,本文通过重写 configure 方法实现自定义配置。
需要注意: WebSecurityConfigurerAdapter 中有多个名称为 configure 重载方法,这里使用的是参数类型为 HttpSecurity 的方法。
注:Spring Security 默认自动化配置参考 Spring Boot Auto Configuration 。
用以指定哪些请求需要什么样的认证或授权,这里使用 anyRequest() 和 authenticated() 表示所有的请求均需要认证。
表示我们使用 HttpBasic 认证。
编译启动应用,会发现终端仍会输出密码:
因为,我们仅仅改变的是认证方式。
为方便演示,我们使用 CURL 直接请求接口:
会提示我们 Unauthorized ,即:没有认证。
我们按照 HttpBasic 要求添加请求头部参数 Authorization ,它的值:
即:
再次请求接口:
认证成功,接口正常响应。
使用默认用户名和随机密码的方式不够灵活,大部分场景都需要我们支持多个用户,且分别为他们设置相应的密码,这就涉及到两个问题:
对于 读取 ,Spring Security 设计了 UserDetailsService 接口:
实现按照用户名(username)从某个存储介质中加载相对应的用户信息(UserDetails)。
用户名,客户端发送请求时写入的用于用户名。
用户信息,包括用户名、密码、权限等相关信息。
注意:用户信息不只用户名和用户密码。
对于 存储 ,Spring Security 设计了 UserDetailsManager 接口:
创建用户信息
修改用户信息
删除用户信息
修改当前用户的密码
检查用户是否存在
注意: UserDetailsManager 继承自 UserDetailsService 。
也就是说,我们可以通过提供一个已实现接口 UserDetailsManager * 的类,并重写其中的若干方法,基于某种存储介质,定义用户名、密码等信息的存储和读取逻辑;然后将这个类的实例以 Bean 的形式注入 Spring Security,就可以实现用户名和密码的自定义。
实际上,Spring Security 仅关心如何 读取 , 存储 可以由业务系统自行实现;相当于,只实现接口 UserDetailsService 即可。
Spring Security 已经为我们预置了两种常见的存储介质实现:
InMemoryUserDetailsManager和 JdbcUserDetailsManager 均实现接口 UserDetailsManager ,本质就是对于 UserDetails 的 CRUD 。我们先介绍 UserDetails ,然后再分别介绍基于内存和数据库的实现。
UserDetails是用户信息的抽象接口:
获取用户名。
获取密码。
获取权限,可以简单理解为角色名称(字符串),用于实现接口基于角色的授权访问,详情见后文。
获取用户是否可用,或用户/密码是否过期或锁定。
Spring Security 提供了一个 UserDetails 的实现类 User ,用于用户信息的实例表示。另外, User 提供 Builder 模式的对象构建方式。
设置用户名称。
设置密码,Spring Security 不建议使用明文字符串存储密码,密码格式:
其中,id 为加密算法标识,encodedPassword 为密码加密后的字符串。这里以加密算法 bcrypt 为例,详细内容可参考 Password Storage 。
设置角色,支持多个。
UserDetails实例创建完成之后,就可以使用 UserDetailsManager 的具体实现进行存储和读取。
InMemoryUserDetailsManager是 Spring Security 为我们提供的基于内存实现的 UserDetailsManager 。
使用 @Bean 将 InMemoryUserDetailsManager 实例注入 Spring Security。
创建 InMemoryUserDetailsManager 实例之后,并不是必须立即调用 createUser 添加用户信息,也可以在业务系统的其它地方获取已注入的 InMemoryUserDetailsManager 动态存储 UserDetails 实例。
编译启动应用,使用我们自己创建的用户名和密码(userA/123456)访问接口:
基于内存介质自定义的用户名和密码已生效,接口正常响应。
JdbcUserDetailsManager是 Spring Security 为我们提供的基于数据库实现的 UserDetailsManager ,相较于 InMemoryUserDetailsManager 使用略复杂,需要我们创建数据表,并准备好数据库连接需要的数据源(DataSource), JdbcUserDetailsManager 实例的创建依赖于数据源。
JdbcUserDetailsManager可以与业务系统共用一个数据库数据源实例,本文不讨论数据源的相关配置。
以 MySQL 为例,创建数据表语句:
其他数据库语句可参考 User Schema 。
JdbcUserDetailsManager实例的创建与注入,除
之外,整体流程与 InMemoryUserDetailsManager 类似,不再赘述。
在业务系统中获取已注入的 JdbcUserDetailsManager 实例,可以动态存储 UserDetails 实例。
编译启动应用,使用我们自己创建的用户名和密码(userA/123456)访问接口:
基于数据库介质自定义的用户名和密码已生效,接口正常响应。
Spring Security 可以提供基于角色的权限控制:
假设,存在两个角色 USER(普通用户) 和 ADMIN(管理员),
角色 USER 可以访问接口 /hello/name,
角色 ADMIN 可以访问接口 /hello/world,
所有用户认证后可以访问接口 /。
我们需要按上述需求重新设置 HttpSecurity :
设置角色 USER 可以访问接口 /hello/name。
设置角色 ADMIN 可以访问接口 /hello/world。
设置其他接口认证后即可访问。
mvcMatchers 支持使用通配符。
创建属于角色 USER 和 ADMIN 的用户:
用户名:userA,密码:123456,角色:USER
用户名:userB,密码:abcdef,角色:ADMIN
对于用户 userA :
使用用户 userA 的用户名和密码访问接口 /:
认证通过,可正常访问。
使用用户 userA 的用户名和密码访问接口 /hello/name:
认证通过,鉴权通过,可正常访问。
使用用户 userA 的用户名和密码访问接口 /hello/world:
认证通过,用户 userA 不属于角色 ADMIN,禁止访问。
使用用户 userA 的用户名和密码访问接口 /:
认证通过,可正常访问。
对于用户 userB :
使用用户 userB 的用户名和密码访问接口 /:
认证通过,可正常访问。
使用用户 userB 的用户名和密码访问接口 /hello/world:
认证通过,鉴权通过,可正常访问。
使用用户 userB 的用户名和密码访问接口 /hello/name:
认证通过,用户 userB 不属于角色 USER,禁止访问。
这里可能会有一点奇怪,一般情况下我们会认为 管理员 应该拥有 普通用户 的全部权限,即普通用户 可以访问接口 /hello/name,那么 管理员 应该也是可以访问接口 /hello/name 的。如何实现呢?
方式一,设置用户 userB 同时拥有角色 USER 和 ADMIN;
这种方式有点不够“优雅”。
方式二,设置角色 ADMIN 包含 USER;
Spring Security 有一个 Hierarchical Roles 的特性,可以支持角色之间的 包含 操作。
使用这个特性要特别注意两个地方:
前文使用的是 HttpSecurity.authorizeHttpRequests 方法,此处需要变更为 HttpSecurity.authorizeRequests 方法。
使用 RoleHierarchy 以 Bean 的方式定义角色之间的 层级关系 ;其中,“ROLE_” 是 Spring Security 要求的固定前缀。
编译启动应用,使用用户 userB 的用户名和密码访问接口 /hello/name:
认证通过,鉴权通过,可正常访问。
如果开启 Spring Security 的 debug 日志级别,访问接口时可以看到如下的日志输出:
可以看出,Spring Security 可以从角色 ADMIN 推导出用户实际拥有 USER 和 ADMIN 两个角色。
Hierarchical Roles 文档中的示例有明显错误:
接口 RoleHierarchy 中并不存在方法 setHierarchy 。前文所述 authorizeRequests 和 RoleHierarchy 结合使用的方法是结合网络搜索和自身实践得出的,仅供参考。
另外, authorizeHttpRequests 和 RoleHierarchy 结合是没有效果的, authorizeRequests 和 authorizeHttpRequests 两者之间的区别可以分别参考 Authorize HttpServletRequests with AuthorizationFilter 和 Authorize HttpServletRequest with FilterSecurityInterceptor 。
鉴权的前提需要认证通过;认证不通过的状态码为401,鉴权不通过的状态码为403,两者是不同的。
Spring Security 异常主要分为两种:认证失败异常和鉴权失败异常,发生异常时会分别使用相应的默认异常处理器进行处理,即:认证失败异常处理器和鉴权失败异常处理器。
使用的认证或鉴权实现机制不同,可能使用的默认异常处理器也不相同。
Spring Security 认证失败异常处理器:
如前文所述,认证失败时,Spring Security 使用默认的认证失败处理器实现返回:
如果想要自定义返回内容,则可以通过自定义认证失败处理器实现:
authenticationEntryPoint()会创建返回一个自定义的 AuthenticationEntryPoint 实例;其中,使用 HttpServletResponse.getWriter().print() 写入我们想要返回的内容:401。
httpBasic().authenticationEntryPoint(authenticationEntryPoint())使用我们自定义的 AuthenticationEntryPoint 替换 HttpBasic 默认的 BasicAuthenticationEntryPoint 。
编译启动应用,使用不正确的用户名和密码访问接口 /:
认证不通过,使用我们自定义的内容 401 返回。
Spring Security 鉴权失败异常处理器:
如前文所述,认证失败时,Spring Security 使用默认的认证失败处理器实现返回:
如果想要自定义返回内容,则可以通过自定义鉴权失败处理器实现:
自定义鉴权失败处理器与认证失败处理器过程类似,不再赘述。
编译启动应用,使用用户 userA 的用户名和密码访问接口 /hello/world:
鉴权不通过,使用我们自定义的内容 403 返回。
exceptionHandling()也是有一个 authenticationEntryPoint() 方法的;对于 HttpBasic 而言,使用 exceptionHandling().authenticationEntryPoint() 设置自定义认证失败处理器是不生效的,具体原因需要大家自行研究。
前文介绍两种认证方式: FormLogin 和 HttpBasic ,Spring Security 还提供其他若干种认证方式,详情可参考 Authentication Mechanisms 。
如果我们想实现自己的认证方式,也是比较简单的。Spring Security 本质就是 过滤器 ,我们可以实现自己的认证过滤器,然后加入到 Spring Security 中即可。
认证过滤器核心实现流程:
除去抛出异常的情况外, filterChain.doFilter(servletRequest, servletResponse); 是必须保证被执行的。
理解认证过滤器涉及的概念会比较多,详情参考 Servlet Authentication Architecture 。
认证过滤器创建完成之后,就可以加入到 Spring Security 中:
Spring Security 根据我们配置的不同,会为我们自动按照一定的次序组装一条 过滤器链 ,通过这条链上的若干过滤器完成认证鉴权的。我们需要把自定义的认证过滤器加到这个链的 合适位置 ,这是选取的位置是在 ExceptionTranslationFilter 的前面。
过滤器链的顺序可以参考 Security Filters 。
ExceptionTranslationFilter 的作用可以参考 Handling Security Exceptions 。
使用自定义认证过滤器时,自定义认证失败异常处理器和鉴权失败异常处理器的设置方法。
编译启动应用,我们会发现可以在不填入任何认证信息的情况下直接访问接口 / 和 /hello/name,因为模拟用户已认证且角色为 USER;访问接口 /hello/world 时会出现提示 403。
Spring Security 自身包含的内容很多,官方文档也不能很好的讲述清楚每个功能特性的使用方法,很多时候需要我们自己根据文档、示例、源码以及他人的分享,尽可能多的实践,逐步加深理解。