最近一直在研究把公司的后端项目登录模块切换成Spring自家的Security安全框架,说下我们现有的登录模块,手机号+验证码登录,登录成功生成一个返回用户信息里面包含Authentication返回给前端,前端下一次请求接口时候Header中携带Authentication,以便后端根据Authentication去Redias中查询用户登录信息是否有效或者是否登录成功;目前的登录模式应该也是大多公司采用的模式。
当我研究使用Security时候发现其实这个框架并不适合这种前后端分离、无状态请求结构项目,为什么这么说呢,Security自家框架本身是采用Session会话管理机制,也就是你登录成功了,下一次请求时候是采用Session去判断你是否登录成功,否则框架是不会让你进到框架里面的,Controller里面的接口方法都进不去,框架直接给拦截了;那么想继续采用Security自家的也要保持现有的也就是我们公司的登录模式也是可以的,我感觉没必要,看完大家应该也会感觉没必要,下面直接上代码,看完你们在评论是否有必要在现有模式上使用Security安全框架。
项目中注意点:
1、登录是使用Controller中的登录,没有使用Security自带的formLogin登录方法,框架的传参是固定的username、password 2个字段,如果登录有多参传递或者参数名字不一致,那就需要重写关于参数方法的class了,所以我们采用Controller中自己的方法块。
2、采用Token持久化,需要写一个拦截器,也就是在你登录成功后请求下一个接口时候不被框架拦截,那么就需要自定义一个拦截器,下一次请求时候要跑在 框架拦截器前面,重新设置下SecurityContextHolder.getContext().setAuthentication(...),否则就被框架拦在外面了,也就是会到框架的没登录回调方法里面了。
@ApiOperation("登录")
@PostMapping(value = "/login")
public ResponseEntity<UserInfoVO> login(@Validated @RequestBody LoginUserLoginVO loginUserLoginVO) throws Exception {
UserInfoVO userInfo = authService.login(loginUserLoginVO);
return new ResponseEntity<>(userInfo, HttpStatus.OK);
}
//业务实现类
@Service
public class AuthServiceImpl implements AuthService {
@Override
public RetLoginUserInfoVO login(LoginUserLoginVO loginUserLoginVO) throws JsonProcessingException {
// 根据手机号判断登陆
User userInfoVO = authMapper.getUserInfoByPhone(loginUserLoginVO.getPhone());
.......省略判断是否登录逻辑
//Security 框架设置 Authentication
//第三个参数传null是因为这个参数是权限参数,我们项目没有用到权限,也就是只有admin才能访问哪些类、只有user用户能访问哪些类设置
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userInfoVO.getPhone(), userInfoVO.getToken(), null);
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authenticate);
return userInfoVO;
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//登录成功、登录失败、没有登录 回调类
private final RequestCallbackHandler callbackHandler;
private final OnceFilter filter;
public SecurityConfig(RequestCallbackHandler callbackHandler,OnceFilter filter) {
this.callbackHandler = callbackHandler;
this.filter = filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定义的那个拦截器
http.addFilterAfter(this.filter, UsernamePasswordAuthenticationFilter.class);
http.headers().cacheControl();
http.cors().and().csrf().disable();
//关掉 session会话管理
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//关掉 Security 自带的login
http.httpBasic().disable();
http.authorizeRequests()
//可不用登录进行访问的接口设置
.antMatchers("/auth/login", "/auth/code")
.permitAll()
//其他接口都需要验证
.anyRequest()
.authenticated()
.and()
//注销
.logout().permitAll()
.logoutSuccessHandler(callbackHandler)
//配置回调接口
.and().exceptionHandling()
.accessDeniedHandler(callbackHandler)
//没有登录就请求其他接口就会回调 commence 提示没有请先登录在访问
.authenticationEntryPoint(callbackHandler);
}
}
//回调类
@Component
public class RequestCallbackHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler,
AuthenticationEntryPoint, AccessDeniedHandler, LogoutSuccessHandler {
private static final Logger log = LoggerFactory.getLogger(AuthServiceImpl.class);
/**
* 登录成功
* 属于 AuthenticationSuccessHandler 的接口
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("【使用Security登录成功回调处】登录成功");
}
/**
* 登录失败
* 属于 AuthenticationFailureHandler 的接口
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
setResponseErrorMessage(exception.getMessage(), HttpStatus.UNAUTHORIZED, response);
log.info("【Security登录失败回调处】登录失败:{}", request.getRequestURI() + " -- " + exception.getMessage());
}
/**
* 没有登录回调
* 属于 AuthenticationEntryPoint 的接口
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.info("【Security没有登录回调】请先登录:{}", request.getRequestURI());
setResponseErrorMessage("请先登录", HttpStatus.UNAUTHORIZED, response);
}
/**
* 无权限访问
* 属于 AccessDeniedHandler 的接口
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("【Security没有权限请求回调】无权访问,请联系管理员:{}", request.getRequestURI());
}
/**
* 退出登录成功
* 属于 LogoutSuccessHandler 的接口
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("【Security退出登录成功回调】退出登录成功:{}", request.getRequestURI());
}
/**
* 异常时候返回给用户端的消息
*/
public void setResponseErrorMessage(String message, HttpStatus status, HttpServletResponse response) throws IOException {
ResponseEntity<Failure> bodyEntity = new ResponseEntity<>(new Failure(message), status);
Failure body = bodyEntity.getBody();
//对象转json反给用户端
String s = FabsBeanUtils.toJsonString(body);
response.setStatus(status.value());
response.getWriter().print(s);
}
}
最后自定义的拦截器类了
@Component
public class OnceFilter extends OncePerRequestFilter {
private final AuthService authService;
private final RequestCallbackHandler handler;
public OnceFilter(AuthService authService, RequestCallbackHandler handler) {
this.authService = authService;
this.handler = handler;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
// 校验用户是否登录
User userInfo = null;
try {
//根据传递的headle中的token去读取redias中是否过期和是否有用户信息
userInfo = authService.authentication(token);
} catch (Exception e) {
e.printStackTrace();
handler.setResponseErrorMessage(e.getMessage(), HttpStatus.UNAUTHORIZED, response);
return;
}
if (userInfo != null) {
//为了保持持久化登录重新设置 security 的 authentication 登录信息验证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userInfo.getPhone(), userInfo.getId(), null);
authenticationToken.setDetails(userInfo);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
大工告成。。。
讨论:如果不采用Security安全框架,自己也去写一个拦截框架,每次请求接口去判断headle中是否有传参,再去redias中去读取用户信息也是可以的,所以这就是我感觉Security不适合我们这种登录模式,适合那种前后端不分离、只给web前端使用的项目,我们项目要给App去使用,那App每次调用接口肯定都是无状态的、不会记住session的,所以没用到Security本身的拦截机制,没体验到框架的自身的安全便利性。
下一期分享一个使用Security框架自身的formLogin登录,并使用接收验证码登录验证的项目。