本文开始讲springsecurity框架登录认证授权的一些知识点。为什么没有说shiro这个框架,主要是现在大部分的主流项目中,特别是前后端分离的项目中权限框架一般都用的是springsecurity,比较适合,然后还有一点,就是一些开源框架(比如最新版本的工作流引擎activiti7)跟springsecurity的整合,促使我对这个springsecurity进一步加深了解。还有一个前提,就是本文是完全基于前后端分离的基础上写作,未分离项目可以做借鉴。
按照正常的思维,一个权限框架要解决的问题是:登录以及还有登录之后的访问。这个需要怎么实现,其实就是一串过滤器跟拦截器。用户没有登录,进行拦截;用户登录之后,带着证书登陆,拦截器先判断是否有证书,然后再判断证书是否合法,有一个不满足,都进行拦截。springsecurity这个框架其实本身封装的就是一连串的过滤器跟拦截器。这里借鉴一下网上的一张原理图片:
首先,我们先说登录。官方术语叫认证Authentication。主要是通过AuthenticationManager接口进行认证。(本文主要将通过用户名密码进行认证,其他认证方式后续文章会有说明。)AuthenticationManager的默认实现是ProviderManager,它又委托AuthenticationProvider实例来实现认证,我们通常用到的认证方式就是通过DaoAuthenticationProvider来认证的。(上边这几句话有点难以理解,实在不理解的话直接跳过,总之就是通过下边这个接口进行认证的,然后登录接口调用这个接口进行认证。)
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)throws AuthenticationException;
}
// 用户登录认证
Authentication authentication = null;
try {
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername(这个实现类后边会有说明,这里主要讲登陆逻辑)
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}catch (Exception e){
if (e instanceof BadCredentialsException) {
//抛出登录异常
throw new UserPasswordNotMatchException();
}else{
//抛出自定义异常
throw new CustomException(e.getMessage());
}
}
接下来,我们要把这套登录整合到我们的系统,需要用到我们自己的用户角色权限表。springsecurity中默认使用UserDetailsService来获取用户权限信息,我们需要自己实现这个接口,然后注入到认证接口中。
public class UserDetailsServiceImpl implements UserDetailsService{
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);//查询用户信息
if (StringUtils.isNull(user)){
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
throw new BaseException("对不起,您的账号:" + username + " 已被删除");
}else if (UserStatus.DISABLE.getCode().equals(user.getStatus())){
throw new BaseException("对不起,您的账号:" + username + " 已停用");
}
//讲用户信息跟权限信息统一封装到UserDetails
new UserDetails(user, permissionService.getMenuPermission(user));
}
}
public class SecurityConfig extends WebSecurityConfigurerAdapter{
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
//注入身份认证接口,通过bCryptPasswordEncoder密码加密认证
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
其次,登录认证都说完了,我们开始说过滤拦截,通过继承自
WebSecurityConfigurerAdapter,对Spring Security自定义配置添加过滤器,下边我们直接在代码里做注释说明:
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private UserDetailsService userDetailsService;//自定义用户认证逻辑
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;//认证失败处理类
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;//退出处理类
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;//token认证过滤器
@Autowired
private CorsFilter corsFilter;//跨域过滤器
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/captchaImage").anonymous()
.antMatchers(HttpMethod.GET,"/*.html","/**/*.html", "/**/*.css", "/**/*.js").permitAll()
.antMatchers("/processDefinition/**").permitAll()
.antMatchers("/activitiHistory/**").permitAll()
.antMatchers("/profile/**").anonymous()
.antMatchers("/common/download**").anonymous()
.antMatchers("/common/download/resource**").anonymous()
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
.antMatchers("/druid/**").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and().headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
}
最后,我们对
JwtAuthenticationTokenFilter过滤器做主要说明,因为这是用户登录之后,每次访问接口的时候,都需要通过这个接口进行token验证,并把用户信息放入到SecurityContextHolder上下文中,然后后台服务就可以直接在上下文中获取用户信息。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
//从请求头中获取token,并从缓存中查询用户信息(缓存中用户信息是在用户登录后放入缓存,可以加快查询效率)
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())){
tokenService.verifyToken(loginUser);//验证token,同时自动刷新token使用时间
//以下逻辑就是把通过token验证的用户信息放入到上下文中,后台服务可以直接通过上下文获取当前用户
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
//后台服务获取当前用户代码
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
以上,就是我对springsecurity安全框架做出的总结。另外,文章里的部分代码,是借鉴若依大佬的RuoyiVue这套前后端分离框架的,完全开源的,有不对的地方,请大家指正。后边文章,我会写前端如何跟后端进行token接口交互的文章,整合前端。