内置方案
spring security 为我们提供了一套登出功能的解决方案,若依正是使用了这套方案来处理登出功能。 打开spring security配置文件
package com.ruoyi.framework.config;
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 允许匿名访问的地址
*/
@Autowired
private PermitAllUrlProperties permitAllUrl;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
// 注解标记允许匿名访问的url
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加Logout filter
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);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
可以看到配置类里面写了很多自定义处理方案,如认证失败处理类和运行匿名访问的url,我们这里只关注登出功能。
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
这个功能的实现本质上拦截器,也就是说spring security内置了一个拦截器去实现登出功能,当然我们也可以不使用该拦截器,去自定义实现登出功能。
logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler)方法接收一个LogoutSuccessHandler接口实现类的实例,此接口也是spring security内置接口,需要我们自己去实现。
public interface LogoutSuccessHandler {
void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
}
重写方法onLogoutSuccess里面写一些登出逻辑,比如删除redis存储用户信息,重定向等
业务流程
知道了spring security的内置处理方法再梳理一下流程。
1 前端向"/logout"发起请求,请求头需要带上token。
2 被拦截器截获去执行LogoutSuccessHandler实现类的重写方法
3 返回相应的信息(若依登出功能有一个bug,很小,虽然无伤大雅,下面细说)
4 前端收到成功信息去将本地的角色集和权限集置为空,再将存储在本地的token删除。
5 重定向到登入页面
具体实现
前端
async logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
location.href = '/index';
})
}).catch(() => {});
}
前端是使用了promise 来处理该请求,在点击弹出框的"确定"按钮后会对后端发起请求,如果成功了会重定向到/index,失败则不进行跳转。
后端
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser))
{
String userName = loginUser.getUsername();
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getToken());
// 记录用户退出日志
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
}
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
}
我们从域对象内获取loginuser对象后进行非空判断,判断通过后去删除redis相应的信息,然后异步的进行日志记录,最后返回成功信息。
退出功能一点小问题
如果我们在前端请求不携带token,后端就无法从域对象内获取用户信息,根据逻辑我们还是会向前端返回成功的信息。
export function logout() {
return request({
url: '/logout',
method: 'post'
})
}
前端登入使用post方法,而我们使用get方法也是可以成功。
也就是说只要我们向这个url发起请求就一定可以返回到成功信息。
改进代码
1 前端向"/logout"发起请求,请求头需要带上token。
2 被拦截器截获去执行LogoutSuccessHandler实现类的重写方法
3 对loginuser进行非空判断,为空则转发到自定义的异常控制器之中(在过滤器上的异常无法被全局异常处理器处理,需要转发到控制器上在抛出异常),抛出异常由自定义全局异常处理器处理
4 非空则返回成功信息
5 前端收到成功信息去将本地的角色集和权限集置为空,再将存储在本地的token删除。
6 重定向到登入页面
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser))
{
String userName = loginUser.getUsername();
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getToken());
// 记录用户退出日志
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
}
request.getRequestDispatcher("/filter/logout").forward(request,response);
}
@RestController
@RequestMapping("/filter")
public class FilterErrorController {
@PostMapping("/logout")
public AjaxResult LogoutError(){
throw new LogoutException("rouyi-framwork","用户未登入");
}
}
@ExceptionHandler(LogoutException.class)
public AjaxResult handleLogoutException(LogoutException e){
return AjaxResult.error(e.getDefaultMessage());
}