以项目驱动学习,以实践检验真知前端
关于认证和受权,R以前已经写了两篇文章:java
📖【项目实践】在用安全框架前,我想先让你手撸一个登录认证git
📖【项目实践】一文带你搞定页面权限、按钮权限以及数据权限github
在这两篇文章中咱们没有使用安全框架就搞定了认证和受权功能,并理解了其核心原理。R在以前就说过,核心原理掌握了,不管什么安全框架使用起来都会很是容易!那么本文就讲解如何使用主流的安全框架Spring Security来实现认证和受权功能。算法
固然,本文并不仅是对框架的使用方法进行讲解,还会剖析Spring Security的源码,看到最后你就会发现你掌握了使用方法的同时,还对框架有了深度的理解!若是没有看过前两篇文章的,强烈建议先看一下,由于安全框架只是帮咱们封装了一些东西,背后的原理是不会变的。spring
本文全部代码都放在了Github上,克隆下来便可运行!数据库
Web系统中登陆认证(Authentication)的核心就是凭证机制,不管是Session
仍是JWT
,都是在用户成功登陆时返回给用户一个凭证,后续用户访问接口需携带凭证来标明本身的身份。后端会对须要进行认证的接口进行安全判断,若凭证没问题则表明已登陆就放行接口,若凭证有问题则直接拒绝请求。这个安全判断都是放在过滤器里统一处理的:json
登陆认证是对用户的身份进行确认,权限受权(Authorization)是对用户可否访问某个资源进行确认,受权发生都认证以后。 认证同样,这种通用逻辑都是放在过滤器里进行的统一操做:后端
LoginFilter
先进行登陆认证判断,认证经过后再由AuthFilter
进行权限受权判断,一层一层没问题后才会执行咱们真正的业务逻辑。跨域
Spring Security对Web系统的支持就是基于这一个个过滤器组成的过滤器链:
用户请求都会通过Servlet
的过滤器链,在以前两篇文章中咱们就是经过自定义的两个过滤器实现了认证受权功能!而Spring Security也是作的一样的事完成了一系列功能:
在Servlet
过滤器链中,Spring Security向其添加了一个FilterChainProxy
过滤器,这个代理过滤器会建立一套Spring Security自定义的过滤器链,而后执行一系列过滤器。咱们能够大概看一下FilterChainProxy
的大体源码:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
...省略其余代码
// 获取Spring Security的一套过滤器
List<Filter> filters = getFilters(request);
// 将这一套过滤器组成Spring Security本身的过滤链,并开始执行
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(request, response);
...省略其余代码
}
复制代码
咱们能够看一下Spring Security默认会启用多少过滤器:
这里面咱们只须要重点关注两个过滤器便可:UsernamePasswordAuthenticationFilter
负责登陆认证,FilterSecurityInterceptor
负责权限受权。
💡Spring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各类组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!这个框架的使用方式就是对这些过滤器和组件进行扩展。
必定要记住这句话,带着这句话去使用和理解Spring Security,你会像站在高处俯瞰,整个框架的脉络一目了然。
刚才咱们总览了一下全局,如今咱们就开始进行代码编写了。
要使用Spring Security确定是要先引入依赖包(Web项目其余必备依赖我在以前文章中已讲解,这里就不过多阐述了):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
复制代码
依赖包导入后,Spring Security就默认提供了许多功能将整个应用给保护了起来:
📝要求通过身份验证的用户才能与应用程序进行交互
📝建立好了默认登陆表单
📝生成用户名为user
的随机密码并打印在控制台上
📝CSRF
攻击防御、Session Fixation
攻击防御
📝等等等等......
在实际开发中,这些默认配置好的功能每每不符合咱们的实际需求,因此咱们通常会自定义一些配置。配置方式很简单,新建一个配置类便可:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
复制代码
在该类中重写WebSecurityConfigurerAdapter
的方法就能对Spring Security进行自定义配置。
依赖包和配置类准备好后,接下来咱们要完成的第一个功能那天然是登陆认证,毕竟用户要使用咱们系统第一步就是登陆。以前文章介绍了Session
和JWT
两种认证方式,这里咱们来用Spring Security实现这两种认证。
无论哪一种认证方式和框架,有些核心概念是不会变的,这些核心概念在安全框架中会以各类组件来体现,了解各个组件的同时功能也就跟着实现了功能。
咱们系统中会有许多用户,确认当前是哪一个用户正在使用咱们系统就是登陆认证的最终目的。这里咱们就提取出了一个核心概念:当前登陆用户/当前认证用户。整个系统安全都是围绕当前登陆用户展开的!这个不难理解,要是当前登陆用户都不能确认了,那A下了一个订单,下到了B的帐户上这不就乱套了。这一律念在Spring Security中的体现就是 💡Authentication
,它存储了认证信息,表明当前登陆用户。
咱们在程序中如何获取并使用它呢?咱们须要经过 💡SecurityContext
来获取Authentication
,看了以前文章的朋友大概就猜到了这个SecurityContext
就是咱们的上下文对象!
这种在一个线程中横跨若干方法调用,须要传递的对象,咱们一般称之为上下文(Context)。上下文对象是很是有必要的,不然你每一个方法都得额外增长一个参数接收对象,实在太麻烦了。
这个上下文对象则是交由 💡SecurityContextHolder
进行管理,你能够在程序任何地方使用它:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
复制代码
能够看到调用链路是这样的:SecurityContextHolder
👉SecurityContext
👉Authentication
。
SecurityContextHolder
原理很是简单,就是和咱们以前实现的上下文对象同样,使用ThreadLocal
来保证一个线程中传递同一个对象!源码我就不贴了,具体可看以前文章写的上下文对象实现。
如今咱们已经知道了Spring Security中三个核心组件:
📝Authentication
:存储了认证信息,表明当前登陆用户
📝SeucirtyContext
:上下文对象,用来获取Authentication
📝SecurityContextHolder
:上下文管理对象,用来在程序任何地方获取SecurityContext
他们关系以下:
Authentication
中那三个玩意就是认证信息:
📝Principal
:用户信息,没有认证时通常是用户名,认证后通常是用户对象
📝Credentials
:用户凭证,通常是密码
📝Authorities
:用户权限
如今咱们知道如何获取并使用当前登陆用户了,那这个用户是怎么进行认证的呢?总不能我随便new一个就表明用户认证完毕了吧。因此咱们还缺一个生成Authentication
对象的认证过程!
认证过程就是登陆过程,不使用安全框架时我们的认证过程是这样的:
查询用户数据👉判断帐号密码是否正确👉正确则将用户信息存储到上下文中👉上下文中有了这个对象则表明该用户登陆了
Spring Security的认证流程也是如此:
Authentication authentication = new UsernamePasswordAuthenticationToken(用户名, 用户密码, 用户的权限集合);
SecurityContextHolder.getContext().setAuthentication(authentication);
复制代码
和不使用安全框架同样,将认证信息放到上下文中就表明用户已登陆。上面代码演示的就是Spring Security最简单的认证方式,直接将Authentication
放置到SecurityContext
中就完成认证了!
这个流程和以前获取当前登陆用户的流程天然是相反的:Authentication
👉SecurityContext
👉SecurityContextHolder
。
是否是以为,就这?这就完成认证啦?这也太简单了吧。对于Spring Security来讲,这样确实就完成了认证,但对于咱们来讲还少了一步,那就是判断用户的帐号密码是否正确。用户进行登陆操做时从会传递过来帐号密码,咱们确定是要查询用户数据而后判断传递过来的帐号密码是否正确,只有正确了我们才会将认证信息放到上下文对象中,不正确就直接提示错误:
// 调用service层执行判断业务逻辑
if (!userService.login(用户名, 用户密码)) {
return "帐号密码错误";
}
// 帐号密码正确了才将认证信息放到上下文中(用户权限须要再从数据库中获取,后面再说,这里省略)
Authentication authentication = new UsernamePasswordAuthenticationToken(用户名, 用户密码, 用户的权限集合);
SecurityContextHolder.getContext().setAuthentication(authentication);
复制代码
这样才算是一个完整的认证过程,和不使用安全框架时的流程是同样的哦,只是一些组件以前是咱们本身实现的。
这里查询用户信息并校验帐号密码是彻底由咱们本身在业务层编写全部逻辑,其实这一块Spring Security也有组件供咱们使用:
💡AuthenticationManager
就是Spring Security用于执行身份验证的组件,只须要调用它的authenticate
方法便可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter
这个过滤器中调用这个组件,该过滤器负责认证逻辑。
咱们要按照本身的方式使用这个组件,先在以前配置类配置一下:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
复制代码
这里咱们写上完整的登陆接口代码:
@RestController
@RequestMapping("/API")
public class LoginController {
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public String login(@RequestBody LoginParam param) {
// 生成一个包含帐号密码的认证信息
Authentication token = new UsernamePasswordAuthenticationToken(param.getUsername(), param.getPassword());
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(token);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
return "登陆成功";
}
}
复制代码
注意,这里流程和以前说的流程是彻底同样的,只是用户身份验证改为了使用
AuthenticationManager
来进行。
AuthenticationManager
的校验逻辑很是简单:
根据用户名先查询出用户对象(没有查到则抛出异常)👉将用户对象的密码和传递过来的密码进行校验,密码不匹配则抛出异常
这个逻辑没啥好说的,再简单不过了。重点是这里每个步骤Spring Security都提供了组件:
📝是谁执行 根据用户名查询出用户对象 逻辑的呢?用户对象数据能够存在内存中、文件中、数据库中,你得肯定好怎么查才行。这一部分就是交由💡UserDetialsService
处理,该接口只有一个方法loadUserByUsername(String username)
,经过用户名查询用户对象,默认实现是在内存中查询。
📝那查询出来的 用户对象 又是什么呢?每一个系统中的用户对象数据都不尽相同,我们须要确认咱们的用户数据是啥样的才行。Spring Security中的用户数据则是由💡UserDetails
来体现,该接口中提供了帐号、密码等通用属性。
📝对密码进行校验你们可能会以为比较简单,if、else
搞定,就不必用什么组件了吧?但框架毕竟是框架考虑的比较周全,除了if、else
外还解决了密码加密的问题,这个组件就是💡PasswordEncoder
,负责密码加密与校验。
咱们能够看下AuthenticationManager
校验逻辑的大概源码:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...省略其余代码
// 传递过来的用户名
String username = authentication.getName();
// 调用UserDetailService的方法,经过用户名查询出用户对象UserDetail(查询不出来UserDetailService则会抛出异常)
UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username);
String presentedPassword = authentication.getCredentials().toString();
// 传递过来的密码
String password = authentication.getCredentials().toString();
// 使用密码解析器PasswordEncoder传递过来的密码是否和真实的用户密码匹配
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
// 密码错误则抛出异常
throw new BadCredentialsException("错误信息...");
}
// 注意哦,这里返回的已认证Authentication,是将整个UserDetails放进去充当Principal
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,
authentication.getCredentials(), userDetails.getAuthorities());
return result;
...省略其余代码
}
复制代码
UserDetialsService
👉UserDetails
👉PasswordEncoder
,这三个组件Spring Security都有默认实现,这通常是知足不了咱们的实际需求的,因此这里咱们本身来实现这些组件!
首先是PasswordEncoder
,这个接口很简单就两个重要方法:
public interface PasswordEncoder {
/** * 加密 */
String encode(CharSequence rawPassword);
/** * 将未加密的字符串(前端传递过来的密码)和已加密的字符串(数据库中存储的密码)进行校验 */
boolean matches(CharSequence rawPassword, String encodedPassword);
}
复制代码
你能够实现此接口定义本身的加密规则和校验规则,不过Spring Security提供了不少加密器实现,咱们这里选定一个就好。能够在以前所说的配置类里进行以下配置:
@Bean
public PasswordEncoder passwordEncoder() {
// 这里咱们使用bcrypt加密算法,安全性比较高
return new BCryptPasswordEncoder();
}
复制代码
由于密码加密是我前面文章少数没有介绍的功能,因此这里额外提一嘴。往数据库中添加用户数据时就要将密码进行加密,不然后续进行密码校验时从数据库拿出来的仍是明文密码,是没法经过校验的。好比咱们有一个用户注册的接口:
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/register")
public String register(@RequestBody UserParam param) {
UserEntity user = new UserEntity();
// 调用加密器将前端传递过来的密码进行加密
user.setUsername(param.getUsername()).setPassword(passwordEncoder.encode(param.getPassword()));
// 将用户实体对象添加到数据库
userService.save(user);
return "注册成功";
}
复制代码
这样数据库中存储的密码都是已加密的了:
该接口就是咱们所说的用户对象,它提供了用户的一些通用属性:
public interface UserDetails extends Serializable {
/** * 用户权限集合(这个权限对象如今无论它,到权限时我会讲解) */
Collection<? extends GrantedAuthority> getAuthorities();
/** * 用户密码 */
String getPassword();
/** * 用户名 */
String getUsername();
/** * 用户没过时返回true,反之则false */
boolean isAccountNonExpired();
/** * 用户没锁定返回true,反之则false */
boolean isAccountNonLocked();
/** * 用户凭据(一般为密码)没过时返回true,反之则false */
boolean isCredentialsNonExpired();
/** * 用户是启用状态返回true,反之则false */
boolean isEnabled();
}
复制代码
实际开发中咱们的用户属性各类各样,这些默认属性必然是知足不了,因此咱们通常会本身实现该接口,而后设置好咱们实际的用户实体对象。实现此接口要重写不少方法比较麻烦,咱们能够继承Spring Security提供的org.springframework.security.core.userdetails.User
类,该类实现了UserDetails
接口帮咱们省去了重写方法的工做:
public class UserDetail extends User {
/** * 咱们本身的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了) */
private UserEntity userEntity;
public UserDetail(UserEntity userEntity, Collection<? extends GrantedAuthority> authorities) {
// 必须调用父类的构造方法,以初始化用户名、密码、权限
super(userEntity.getUsername(), userEntity.getPassword(), authorities);
this.userEntity = userEntity;
}
}
复制代码
该接口很简单只有一个方法:
public interface UserDetailsService {
/** * 根据用户名获取用户对象(获取不到直接抛异常) */
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
复制代码
我们本身的用户业务类该接口便可完成本身的逻辑:
@Service
public class UserServiceImpl implements UserService, UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) {
// 从数据库中查询出用户实体对象
UserEntity user = userMapper.selectByUsername(username);
// 若没查询到必定要抛出该异常,这样才能被Spring Security的错误处理器处理
if (user == null) {
throw new UsernameNotFoundException("没有找到该用户");
}
// 走到这表明查询到了实体对象,那就返回咱们自定义的UserDetail对象(这里权限暂时放个空集合,后面我会讲解)
return new UserDetail(user, Collections.emptyList());
}
}
复制代码
AuthenticationManager
校验所调用的三个组件咱们就已经作好实现了!
不知道你们注意到没有,当咱们查询用户失败时或者校验密码失败时都会抛出Spring Security的自定义异常。这些异常不可能听任无论,Spring Security对于这些异常都是在ExceptionTranslationFilter
过滤器中进行处理(能够回顾一下前面的过滤器截图),而💡AuthenticationEntryPoint
则专门处理认证异常!
该接口也只有一个方法:
public interface AuthenticationEntryPoint {
/** * 接收异常并处理 */
void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException);
}
复制代码
咱们来自定义一个类实现咱们本身的错误处理逻辑:
public class MyEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
// 直接提示前端认证错误
out.write("认证错误");
out.flush();
out.close();
}
}
复制代码
用户传递过来帐号密码👉认证校验👉异常处理,这一整套流程的组件咱们就都给定义完了!如今只差最后一步,就是在Spring Security配置类里面进行一些配置,才能让这些生效。
Spring Security对哪些接口进行保护、什么组件生效、某些功能是否启用等等都须要在配置类中进行配置,注意看代码注释:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf和frameOptions,若是不关闭会影响前端请求接口(这里不展开细讲了,感兴趣的自行了解)
http.csrf().disable();
http.headers().frameOptions().disable();
// 开启跨域以便前端调用接口
http.cors();
// 这是配置的关键,决定哪些接口开启防御,哪些接口绕过防御
http.authorizeRequests()
// 注意这里,是容许前端跨域联调的一个必要配置
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
// 指定某些接口不须要经过验证便可访问。登录、注册接口确定是不须要认证的
.antMatchers("/API/login", "/API/register").permitAll()
// 这里意思是其它全部接口须要认证才能访问
.anyRequest().authenticated()
// 指定认证错误处理器
.and().exceptionHandling().authenticationEntryPoint(new MyEntryPoint());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService和加密器
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
复制代码
其中用的最多的就是configure(HttpSecurity http)
方法,能够经过HttpSecurity
进行许多配置。当咱们重写这个方法时,就已经关闭了默认的表单登陆方式,而后咱们再配置好启用哪些组件、指定哪些接口须要认证,就搞定了!
假设如今咱们有一个/API/test
接口,在没有登陆的时候调用该接口看下效果:
咱们登陆一下:
而后再调用测试接口:
能够看到未登陆时测试接口是没法正常访问的,会按照咱们在EntryPoint
中的逻辑返回错误提示。
有人可能会问,用AuthenticationManager
认证方式要配置好多东西啊,我就用以前说的那种最简单的方式不行吗?固然是能够的啦,用哪一种方式都随便,只要完成功能都行。其实无论哪一种方式咱们的认证的逻辑代码同样都没少,只不过一个是咱们本身业务类所有搞定,一个是能够集成框架的组件。这里也顺带再总结一下流程:
AuthenticationManager
UserDetailService
查询出UserDetails
PasswordEncoder
UserDetails
存入到Authentication
,将Authentication
存入到SecurityContext
AuthenticationEntryPoint
处理刚才咱们讲的认证方式都是基于session
机制,认证后Spring Security会将包含了认证信息的SecurityContext
存入到session
中,Key为HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY
。也就是说,你彻底能够经过以下方式获取SecurityContext
:
SecurityContext securityContext= (SecurityContext)session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)
复制代码
固然,官方仍是不推荐这样直接操做的,由于统一经过SecurityContextHolder
操做更利于管理!使用SecurityContextHolder
除了获取当前用户外,退出登陆的操做也是很方便的:
@GetMapping("/logout")
public String logout() {
SecurityContextHolder.clearContext();
return "退出成功";
}
复制代码
session
认证我们就讲解到此,接下来我们讲解JWT
的认证。
关于JWT
的介绍和工具类等我在前面文章已经讲的很清楚了,这里我就不额外说明了,直接带你们实现代码。
采用JWT
的方式进行认证首先作的第一步就是在配置类里禁用掉session
:
// 禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
复制代码
注意,这里的禁用是指Spring Security不采用
session
机制了,不表明你禁用掉了整个系统的session
功能。
而后咱们再修改一下登陆接口,当用户登陆成功的同时,咱们须要生成token
并返回给前端,这样前端才能访问其余接口时携带token
:
@Autowired
private UserService userService;
@PostMapping("/login")
public UserVO login(@RequestBody @Validated LoginParam user) {
// 调用业务层执行登陆操做
return userService.login(user);
}
复制代码
业务层方法:
public UserVO login(LoginParam param) {
// 根据用户名查询出用户实体对象
UserEntity user = userMapper.selectByUsername(param.getUsername());
// 若没有查到用户 或者 密码校验失败则抛出自定义异常
if (user == null || !passwordEncoder.matches(param.getPassword(), user.getPassword())) {
throw new ApiException("帐号密码错误");
}
// 须要返回给前端的VO对象
UserVO userVO = new UserVO();
userVO.setId(user.getId())
.setUsername(user.getUsername())
// 生成JWT,将用户名数据存入其中
.setToken(jwtManager.generate(user.getUsername()));
return userVO;
}
复制代码
咱们执行一下登陆操做:
咱们能够看到登陆成功时接口会返回token
,后续咱们再访问其它接口时须要将token
放到请求头中。这里咱们须要自定义一个认证过滤器,来对token
进行校验:
@Component
public class LoginFilter extends OncePerRequestFilter {
@Autowired
private JwtManager jwtManager;
@Autowired
private UserServiceImpl userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 从请求头中获取token字符串并解析(JwtManager以前文章有详解,这里很少说了)
Claims claims = jwtManager.parse(request.getHeader("Authorization"));
if (claims != null) {
// 从`JWT`中提取出以前存储好的用户名
String username = claims.getSubject();
// 查询出用户对象
UserDetails user = userService.loadUserByUsername(username);
// 手动组装一个认证对象
Authentication authentication = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
// 将认证对象放到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
复制代码
过滤器中的逻辑和以前介绍的最简单的认证方式逻辑是一致的,每当一个请求来时咱们都会校验JWT
进行认证,上下文对象中有了Authentication
后续过滤器就会知道该请求已经认证过了。
我们这个自定义的过滤器须要替换掉Spring Security默认的认证过滤器,这样咱们的过滤器才能生效,因此咱们须要进行以下配置:
// 将咱们自定义的认证过滤器插入到默认的认证过滤器以前
http.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);
复制代码
咱们能够断点调试看一下如今的过滤器是怎样的:
能够看到咱们自定义的过滤器已经在过滤器链中,由于没有启用表单认证因此UsernamePasswordAuthenticationFilter
不会生效。
携带token
访问接口时能够查看效果:
登陆认证到此就讲解完毕了,接下来咱们一气呵成来实现权限受权!
菜单权限主要是经过前端渲染,数据权限主要靠SQL
拦截,和Spring Security没太大耦合,就很少展开了。咱们来梳理一下接口权限的受权的流程:
完成了登陆认证功能后,想必你们已经有点感受:Spring Security将流程功能分得很细,每个小功能都会有一个组件专门去作,咱们要作的就是去自定义这些组件!Spring Security针对上述流程也提供了许多组件。
Spring Security的受权发生在FilterSecurityInterceptor
过滤器中:
SecurityMetadataSource
,来获取当前请求的鉴权规则Authentication
获取当前登陆用户全部权限数据:💡GrantedAuthority
,这个咱们前面提过,认证对象里存放这权限数据AccessDecisionManager
来校验当前用户是否拥有该权限AccessDeniedHandler
处理咱们能够来看一下过滤器里大概的源码:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
...省略其它代码
// 这是Spring Security封装的对象,该对象里包含了request等信息
FilterInvocation fi = new FilterInvocation(request, response, chain);
// 这里调用了父类的AbstractSecurityInterceptor的方法,认证核心逻辑基本全在父类里
InterceptorStatusToken token = super.beforeInvocation(fi);
...省略其它代码
}
复制代码
父类的beforeInvocation
大概源码以下:
protected InterceptorStatusToken beforeInvocation(Object object) {
...省略其它代码
// 调用SecurityMetadataSource来获取当前请求的鉴权规则,这个ConfigAttribue就是规则,后面我会讲
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// 若是当前请求啥规则也没有,就表明该请求无需受权便可访问,直接结束方法
if (CollectionUtils.isEmpty(attributes)) {
return null;
}
// 获取当前登陆用户
Authentication authenticated = authenticateIfRequired();
// 调用AccessDecisionManager来校验当前用户是否拥有该权限,没有权限则抛出异常
this.accessDecisionManager.decide(authenticated, object, attributes);
...省略其它代码
}
复制代码
老生常谈,核心流程都是同样的。咱们接下来自定义这些组件,以完成咱们本身的鉴权逻辑。
该接口咱们只须要关注一个方法:
public interface SecurityMetadataSource {
/** * 获取当前请求的鉴权规则 * @param object 该参数就是Spring Security封装的FilterInvocation对象,包含了不少request信息 * @return 鉴权规则对象 */
Collection<ConfigAttribute> getAttributes(Object object);
}
复制代码
ConfigAttribute
就是咱们所说的鉴权规则,该接口只有一个方法:
public interface ConfigAttribute {
/** * 这个字符串就是规则,它能够是角色名、权限名、表达式等等。 * 你彻底能够按照本身想法来定义,后面AccessDecisionManager会用这个字符串 */
String getAttribute();
}
复制代码
在以前文章中咱们受权的实现全是靠着资源id
,用户id
关联角色id
,角色id
关联资源id
,这样用户就至关于关联了资源,而咱们接口资源在数据库中的体现是这样的:
这里仍是同样,咱们照样以资源id
做为权限的标记。接下我们就来自定义SecurityMetadataSource
组件:
@Component
public class MySecurityMetadataSource implements SecurityMetadataSource {
/** * 当前系统全部接口资源对象,放在这里至关于一个缓存的功能。 * 你能够在应用启动时将该缓存给初始化,也能够在使用过程当中加载数据,这里我就很少展开说明了 */
private static final Set<Resource> RESOURCES = new HashSet<>();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) {
// 该对象是Spring Security帮咱们封装好的,能够经过该对象获取request等信息
FilterInvocation filterInvocation = (FilterInvocation) object;
HttpServletRequest request = filterInvocation.getRequest();
// 遍历全部权限资源,以和当前请求进行匹配
for (Resource resource : RESOURCES) {
// 由于咱们url资源是这种格式:GET:/API/user/test/{id},冒号前面是请求方法,冒号后面是请求路径,因此要字符串拆分
String[] split = resource.getPath().split(":");
// 由于/API/user/test/{id}这种路径参数不能直接equals来判断请求路径是否匹配,因此须要用Ant类来匹配
AntPathRequestMatcher ant = new AntPathRequestMatcher(split[1]);
// 若是请求方法和请求路径都匹配上了,则表明找到了这个请求所需的权限资源
if (request.getMethod().equals(split[0]) && ant.matches(request)) {
// 将咱们权限资源id返回,这个SecurityConfig就是ConfigAttribute一个简单实现
return Collections.singletonList(new SecurityConfig(resource.getId().toString()));
}
}
// 走到这里就表明该请求无需受权便可访问,返回空
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
// 不用管,这么写就行
return null;
}
@Override
public boolean supports(Class<?> clazz) {
// 不用管,这么写就行
return true;
}
}
复制代码
注意,咱们这里返回的ConfigAttribute
鉴权规则,就是咱们的资源id
。
该组件表明用户所拥有的权限,和ConfigAttribute
同样也只有一个方法,该方法返回的字符串就是表明着权限
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
复制代码
将GrantedAuthority
和ConfigAttribute
一对比,就知道用户是否拥有某个权限了。
Spring Security对GrantedAuthority
有一个简单实现SimpleGrantedAuthority
,对我们来讲够用了,因此咱们额外再新建一个实现。咱们要作的就是在UserDetialsService
中,获取用户对象的同时也将权限数据查询出来:
@Override
public UserDetails loadUserByUsername(String username) {
UserEntity user = userMapper.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("没有找到该用户");
}
// 先将该用户所拥有的资源id所有查询出来,再转换成`SimpleGrantedAuthority`权限对象
Set<SimpleGrantedAuthority> authorities = resourceService.getIdsByUserId(user.getId())
.stream()
.map(String::valueOf)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
// 将用户实体和权限集合都放到UserDetail中,
return new UserDetail(user, authorities);
}
复制代码
这样当认证完毕时,Authentication
就会拥有用户信息和权限数据了。
终于要来到咱们真正的受权组件了,这个组件才最终决定了你有没有某个权限,该接口咱们只需关注一个方法:
public interface AccessDecisionManager {
/** * 受权操做,若是没有权限则抛出异常 * * @param authentication 当前登陆用户,以获取当前用户权限信息 * @param object FilterInvocation对象,以获取request信息 * @param configAttributes 当前请求鉴权规则 */
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
}
复制代码
该方法接受了这几个参数后彻底能作到权限校验了,咱们来实现本身的逻辑:
@Component
public class MyDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) {
// 若是受权规则为空则表明此URL无需受权就能访问
if (Collections.isEmpty(configAttributes)) {
return;
}
// 判断受权规则和当前用户所属权限是否匹配
for (ConfigAttribute ca : configAttributes) {
for (GrantedAuthority authority : authentication.getAuthorities()) {
// 若是匹配上了,表明当前登陆用户是有该权限的,直接结束方法
if (Objects.equals(authority.getAuthority(), ca.getAttribute())) {
return;
}
}
}
// 走到这里就表明没有权限,必需要抛出异常,不然错误处理器捕捉不到
throw new AccessDeniedException("没有相关权限");
}
@Override
public boolean supports(ConfigAttribute attribute) {
// 不用管,这么写就行
return true;
}
@Override
public boolean supports(Class<?> clazz) {
// 不用管,这么写就行
return true;
}
}
复制代码
该组件和以前的认证异常处理器同样,只有一个方法用来处理异常,只不过这个是用来处理受权异常的。咱们直接来实现:
public class MyDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
out.write("没有相关权限");
out.flush();
out.close();
}
}
复制代码
组件都定义好了,那咱们接下来就是最后一步咯,就是让这些组件生效。咱们的鉴权规则源组件SecurityMetadataSource
和受权管理组件AccessDecisionManager
必须经过鉴权过滤器FilterSecurityInterceptor
来配置生效,因此咱们得本身先写一个过滤器,这个过滤器的核心代码基本按照父类的写就行,主要就是属性的配置:
@Component
public class AuthFilter extends AbstractSecurityInterceptor implements Filter {
@Autowired
private SecurityMetadataSource securityMetadataSource;
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
// 将咱们自定义的SecurityMetadataSource给返回
return this.securityMetadataSource;
}
@Override
@Autowired
public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
// 将咱们自定义的AccessDecisionManager给注入
super.setAccessDecisionManager(accessDecisionManager);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 下面的就是按照父类写法写的
FilterInvocation fi = new FilterInvocation(request, response, chain);
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
// 请求以后的处理
super.afterInvocation(token, null);
}
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
}
复制代码
过滤器定义好了,咱们回到Spring Security配置类让这个过滤器插入到原有的鉴权过滤器以前就一切都搞定啦:
http.addFilterBefore(authFilter, FilterSecurityInterceptor.class);
复制代码
咱们能够来看下效果,没有权限的状况下访问接口:
有权限的状况下访问接口:
整个Spring Security就讲解完毕了,咱们对两个过滤器、N多个组件进行了自定义实现,从而达到了咱们的功能。这里作了一个思惟导图方便你们理解:
别看组件这么多,认证受权的核心流程和一些概念是不会变的,什么安全框架都万变不离其宗。好比Shiro
,其中最基本的概念Subject
就表明当前用户,SubjectManager
就是用户管理器……
在我前两篇文章中有人也谈到用安全框架还不如本身手写,确实,手写能够最大灵活度按照本身的想法来(而且也不复杂),使用安全框架反而要配合框架的定式,好像被束缚了。那安全框架对比手写有什么优点呢?我以为优点主要有以下两点:
讲解到这里就结束了,本文全部代码、SQL
语句都放在Github,克隆下来便可运行。
转载请联系公众号【RudeCrab】开启白名单