【实现思路】
自定义过滤器继承UsernamePasswordAuthenticationFilter
,可以配置一个需要身份验证的请求匹配器,它使用AntPathRequestMatcher来匹配请求的URL路径。(如果请求的URL路径与"/user/login"相匹配,并且HTTP方法为POST,那么该请求将被要求进行身份验证)
【代码实现】
1. 编写Security配置类WebSecurityConfig
java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法级别的权限认证
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${spring.security.concurrent-session-control.maximum-sessions}")
private int maximumSessions;
@Value("${spring.security.concurrent-session-control.exception-if-maximum-exceeded:false}")
private boolean exceptionIfMaximumExceeded;
@Value("${server.servlet.session.cookie.name:}")
private String cookieId;
@Autowired
private JsonMapper jsonMapper;
@Autowired
@Qualifier("customUserDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Autowired
private SysUserService userMgrService;
@Autowired
private SysLogService sysLogService;
@Autowired
private SipacUidService sipacUidService;
@Autowired
private SipacZwxtService sipacZwxtService;
@Autowired
private UserLoginPwdErrorService userLoginPwdErrorService;
/**
* 会话存储库
*/
@Autowired
private FindByIndexNameSessionRepository<?> sessionRepository;
/**
* 定义无需权限的接口列表(requestMethod空格requestUrl),在CustomUserDetailsService中使用
*
* @return
*/
@Bean
public Set<GrantedAuthority> publicAuthority() {
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
grantedAuthorities.add(new SimpleGrantedAuthority("GET /user/info"));
grantedAuthorities.add(new SimpleGrantedAuthority("GET /common/bell"));
grantedAuthorities.add(new SimpleGrantedAuthority("POST /user/changePwd"));
return grantedAuthorities;
}
/**
* 身份验证管理生成器
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 自定义customUserDetailsService必须单独创建并注入Spring,否则Spring检查不到此bean,仍然会启动inMemoryAuthentication
auth.userDetailsService(userDetailsService)
// 使用 BCryptPasswordEncoder 加盐加密
.passwordEncoder(new BCryptPasswordEncoder());
/// //不删除凭据,以便记住用户
// auth.eraseCredentials(false);
}
/**
* HTTP请求安全处理
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception { //HTTP请求安全处理
http
.authorizeRequests() // 授权
//允许直接访问的地址(其中根路径/需要放开,不然将vue打包后放入java项目路径一起发布后将无法访问根路径)
.antMatchers( "/app/**","/common/**", "/zwxt/api/**", "/zwxtLogin.html", "/mobile/", "/manualTask/**").permitAll()
//将PreflightRequest不做拦截, 放过 option 请求
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
//使用Spring EL表达式规则 将自定义权限规则传入,所有url必须走我们写的权限规则方法,EL表达式为true时才能访问
.anyRequest().access("@rbcaService.hasPermission(request,authentication)")
//异常处理
.and()
.exceptionHandling()
//没有权限,返回json
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException ex) throws IOException, ServletException {
handleAccessDenied(req, resp, ex);
}
})
// 设置登录失效处理程序
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException ex) throws IOException, ServletException {
handleAuthenticationEntryPointCommence(req, resp, ex);
}
})
// 退出登录
.and()
.logout()
// 退出登录的url, 默认为/logout
.logoutUrl("/user/logout")
// .logoutSuccessUrl("/logout/success").permitAll() // 退出成功跳转URL,注意该URL不需要权限验证
.addLogoutHandler(new LogoutHandler() {
@Override
public void logout(HttpServletRequest req, HttpServletResponse resp, Authentication auth) {
handleLogout(req, resp, auth);
// 退出时需要移除,否则下次登录时会提示超过设定的最大session会话数
sessionRegistry().removeSessionInformation(req.getSession().getId());
}
})
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException, ServletException {
handleLogoutSuccess(req, resp, auth);
}
})
// 退出登录删除指定的cookie
.deleteCookies(cookieId)
.and()
.csrf().disable() //取消csrf防护,否则登录报错403,权限不足
.cors().disable(); //开启跨域访问
// 同源允许iframe嵌套
http.headers().frameOptions().sameOrigin();
// 禁用缓存
http.headers().cacheControl();
// // 以下设置无效,因为已经用添加自定义ConcurrentSessionFilter代替
// //session相关的控制
// http.sessionManagement()
// //指定最大的session并发数量---即一个用户只能同时在一处登陆(腾讯视频的账号好像就只能同时允许2-3个手机同时登陆)
// .maximumSessions(1)
// //当超过指定的最大session并发数量时,阻止后面的登陆(感觉貌似很少会用到这种策略)
// .maxSessionsPreventsLogin(true)
// ;
//用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter,从JSON中获取用户名密码进行验证
http.addFilterAt(jsonUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAt(zwxtLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
/*
ConcurrentSessionFilter:判断session是否过期,如果过期就执行过期方法。
本来这个过滤器是不需要我们管的,但是这个过滤器中也用到了SessionRegistry,而SessionRegistry现在是由我们自己来定义的,所以该过滤器我们也要重新配置一下
*/
http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), new JsonSessionInformationExpiredStrategy(jsonMapper)), ConcurrentSessionFilter.class);
}
/**
* spring session 会话注册表
*
* @return
*/
@Bean
public SpringSessionBackedSessionRegistry<?> sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
}
@Bean
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter() throws Exception {
// 自定义Filter,从JSON中获取用户名密码进行验证
JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(jsonMapper, userLoginPwdErrorService);
filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login", HttpMethod.POST.name()));
// // 将存有的身份信息传进去, 重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
// filter.setAuthenticationManager(super.authenticationManagerBean());
// // 自定义AuthenticationManager,实现认证方法
filter.setAuthenticationManager(new ProviderManager(Collections.singletonList(
new JsonUsernamePasswordAuthenticationProvider(userDetailsService, passwordEncoder(), sipacUidService, userLoginPwdErrorService))));
// session并发控制,因为默认的并发控制方法是空方法.这里必须自己配置一个,并且设置setMaximumSessions和setExceptionIfMaximumExceeded
// 这两个设置值和http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true)作用一样,
// 但在自定义Filter中不会取http.sessionManagement()中设置的值,需要在这里设置
// 如果需要根据用户某个状态来判断,如果用户处在空闲状态,就允许同一个账号后登录的踢掉前面登录的,如果是繁忙状态就不允许登录,请参考
// https://blog.csdn.net/huangmingleiluo/article/details/78546502 参照ConcurrentSessionControlAuthenticationStrategy自定义类实现SessionAuthenticationStrategy接口即可
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy =
new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
//设置单账号最大并发登录数
concurrentSessionControlAuthenticationStrategy.setMaximumSessions(maximumSessions);
/*
为true时,当超过指定的最大session并发数量时,阻止后面的登陆;
为false时,将最近使用最少的若干(当前会话总数(含当前会话)-允许的最大数+1)会话其标记为无效
具体见org.springframework.security.web.authentication.session.allowableSessionsExceeded
*/
concurrentSessionControlAuthenticationStrategy.setExceptionIfMaximumExceeded(exceptionIfMaximumExceeded);
//组合多个SessionAuthenticationStrategy,
CompositeSessionAuthenticationStrategy delegateStrategies = new CompositeSessionAuthenticationStrategy(
//要注意这些实例在集合中的顺序
Arrays.asList(
concurrentSessionControlAuthenticationStrategy,
/*
固定会话攻击保护策略
ChangeSessionIdAuthenticationStrategy仍使用原来的session的SessionAuthenticationStrategy,仅修改下session id;
SessionFixationProtectionStrategy则是创建一个新的session,将老session的属性迁移到新的session中。
只有当请求中已经存在session,并且是有效的时候才会进行实际的处理,具体见这两个类的父类AbstractSessionFixationProtectionStrategy的onAuthentication方法。
*/
new SessionFixationProtectionStrategy(),
/*
注册新session信息到SessionRegistry,在这里注册了,在JsonUsernamePasswordAuthenticationFilter中就不需要
sessionRegistry.registerNewSession(request.getSession().getId(), authRequest.getPrincipal());了。
在这注册是验证成功后注册的,而JsonUsernamePasswordAuthenticationFilter则是在验证之前
*/
new RegisterSessionAuthenticationStrategy(sessionRegistry()
)
));
filter.setSessionAuthenticationStrategy(delegateStrategies);
filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {
handleAuthenticationSuccess(req, resp, auth);
}
});
filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
handleAuthenticationFailure(req, resp, e);
}
});
return filter;
}
@Bean
ZwxtLoginAuthenticationFilter zwxtLoginAuthenticationFilter() throws Exception {
// 自定义Filter,从JSON中获取Token进行验证
ZwxtLoginAuthenticationFilter filter = new ZwxtLoginAuthenticationFilter(jsonMapper, sipacZwxtService);
filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/zwxtLogin", HttpMethod.POST.name()));
//自定义AuthenticationManager,实现认证方法
filter.setAuthenticationManager(new ProviderManager(Collections.singletonList(
new ZwxtAuthenticationProvider(userDetailsService, passwordEncoder(), sipacZwxtService))));
// session并发控制,因为默认的并发控制方法是空方法.这里必须自己配置一个,并且设置setMaximumSessions和setExceptionIfMaximumExceeded
// 这两个设置值和http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true)作用一样,
// 但在自定义Filter中不会取http.sessionManagement()中设置的值,需要在这里设置
// 如果需要根据用户某个状态来判断,如果用户处在空闲状态,就允许同一个账号后登录的踢掉前面登录的,如果是繁忙状态就不允许登录,请参考
// https://blog.csdn.net/huangmingleiluo/article/details/78546502 参照ConcurrentSessionControlAuthenticationStrategy自定义类实现SessionAuthenticationStrategy接口即可
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy =
new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
//设置单账号最大并发登录数
concurrentSessionControlAuthenticationStrategy.setMaximumSessions(maximumSessions);
/*
为true时,当超过指定的最大session并发数量时,阻止后面的登陆;
为false时,将最近使用最少的若干(当前会话总数(含当前会话)-允许的最大数+1)会话其标记为无效
具体见org.springframework.security.web.authentication.session.allowableSessionsExceeded
*/
concurrentSessionControlAuthenticationStrategy.setExceptionIfMaximumExceeded(exceptionIfMaximumExceeded);
//组合多个SessionAuthenticationStrategy,
CompositeSessionAuthenticationStrategy delegateStrategies = new CompositeSessionAuthenticationStrategy(
//要注意这些实例在集合中的顺序
Arrays.asList(
concurrentSessionControlAuthenticationStrategy,
/*
固定会话攻击保护策略
ChangeSessionIdAuthenticationStrategy仍使用原来的session的SessionAuthenticationStrategy,仅修改下session id;
SessionFixationProtectionStrategy则是创建一个新的session,将老session的属性迁移到新的session中。
只有当请求中已经存在session,并且是有效的时候才会进行实际的处理,具体见这两个类的父类AbstractSessionFixationProtectionStrategy的onAuthentication方法。
*/
new SessionFixationProtectionStrategy(),
/*
注册新session信息到SessionRegistry,在这里注册了,在JsonUsernamePasswordAuthenticationFilter中就不需要
sessionRegistry.registerNewSession(request.getSession().getId(), authRequest.getPrincipal());了。
在这注册是验证成功后注册的,而JsonUsernamePasswordAuthenticationFilter则是在验证之前
*/
new RegisterSessionAuthenticationStrategy(sessionRegistry()
)
));
filter.setSessionAuthenticationStrategy(delegateStrategies);
filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {
handleAuthenticationSuccess(req, resp, auth);
}
});
filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException {
handleAuthenticationFailure(req, resp, e);
}
});
return filter;
}
@Override
public void configure(WebSecurity web) { //WEB安全
//设置静态资源不要拦截
web.ignoring().antMatchers("/static/**");
//对于在header里面增加token等类似情况,放行所有OPTIONS请求。
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
/**
* 密码生成策略.
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/*
用户成功进行身份验证后的处理器。当用户成功登录并通过身份验证时,身份验证成功处理器将被调用,以执行特定的操作或逻辑。通常用于在用户登录成功后,跳转到指定页面、记录日志、发送通知等操作。
*/
private void handleAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {
UserDetails userDetails = (UserDetails) auth.getPrincipal();
String userId = userDetails.getUsername();
HttpSession session = req.getSession();
session.setAttribute(ConstantUtil.SESSION_USER_ID, userId);
resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
resp.setCharacterEncoding("UTF-8");
Map<String, Object> map = new HashMap<>(1);
map.put("token", req.getSession().getId());
PrintWriter out = resp.getWriter();
SysUser sysUser = userMgrService.get(userId);
String message;
if (sysUser.getAccountType() == AccountTypeEnum.PT && passwordEncoder().matches("123456", sysUser.getPwd())) {
message = "登录成功! 当前密码为初始密码,请尽快修改!";
} else {
message = "登录成功!";
}
out.write(jsonMapper.writeValueAsString(Response.ok(message, map)));
out.flush();
out.close();
//获取请求IP
String ip = StringUtils.getIpAddr(req);
//获取请求Method
String method = req.getMethod();
//获取请求Url
String url = req.getRequestURI();
LocalDateTime now = LocalDateTime.now();
//更新用户最新登录时间和IP
try {
userMgrService.updateLastLogin(userId, now, ip);
} catch (Exception e) {
log.error("更新用户最新登录时间和IP失败", e);
}
//记录日志到数据库
try {
sysLogService.save(new SysLog(userId, now, ip, "登录", "登录", method, url, null, ConstantUtil.SUCCESS,
null, null, null));
} catch (Exception e) {
log.error("记录操作日志失败", e);
}
}
/*
用户身份验证失败时的处理器。当用户尝试进行身份验证但失败时,身份验证失败处理器将被调用,以执行特定的操作或逻辑。通常用于在用户登录失败后,记录日志、显示错误消息、重定向到登录页面等操作。
*/
private void handleAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException {
resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
resp.setCharacterEncoding("UTF-8");
Response<?> response;
if (e instanceof PasswordErrorBeforeAccountLockException) {
PasswordErrorBeforeAccountLockException o = (PasswordErrorBeforeAccountLockException) e;
response = Response.error("密码错误,再输错" + o.getErrorRemainNum() + "次,账号将被临时锁定" + o.getLockTime() + "分钟");
} else if (e instanceof PasswordErrorOverLimitAccountLockException) {
PasswordErrorOverLimitAccountLockException o = (PasswordErrorOverLimitAccountLockException) e;
response = Response.error("密码错误次数超过限制,账号临时锁定" + o.getLockTime() + "分钟");
} else if (e instanceof AccountLockException) {
AccountLockException o = (AccountLockException) e;
response = Response.error("账号已被临时锁定,请" + o.getLockRemainTime() + "分钟后再试");
} else if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) {
response = Response.error("用户名或者密码输入错误!");
} else if (e instanceof LockedException) {
response = Response.error("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
response = Response.error("密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
response = Response.error("账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
response = Response.error("账户被禁用,请联系管理员!");
} else if (e instanceof SessionAuthenticationException) {
response = Response.error("登录失败! 当前用户超过最大并发登录数!");
} else if (e instanceof SipacUidValidateException) {
response = Response.error("zw用户体系接口:" + e.getMessage());
} else if (e instanceof SipacZwxtValidateException) {
response = Response.error("zw系统认证中心接口:" + e.getMessage());
} else if (e instanceof AuthenticationServiceException) {
response = Response.error("登录失败! " + e.getMessage());
} else {
response = Response.error("登录失败! " + e.getMessage());
}
PrintWriter out = resp.getWriter();
out.write(jsonMapper.writeValueAsString(response));
out.flush();
out.close();
}
private void handleAccessDenied(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException {
resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
out.write(jsonMapper.writeValueAsString(Response.error(ResultCode.NotAuthorized)));
out.flush();
out.close();
}
private void handleAuthenticationEntryPointCommence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException {
resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
out.write(jsonMapper.writeValueAsString(Response.error(ResultCode.NoLoginOrSessionExpired)));
out.flush();
out.close();
}
private void handleLogout(HttpServletRequest req, HttpServletResponse resp, Authentication auth) {
//当session过期或者用户清除浏览器缓存后,点击退出按钮,此处无法获取租户和用户标识信息,也就无法记录日志了
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return;
}
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String userId = userDetails.getUsername();
//获取请求IP
String ip = StringUtils.getIpAddr(req);
//获取请求Method
String method = req.getMethod();
//获取请求Url
String url = req.getRequestURI();
LocalDateTime now = LocalDateTime.now();
//记录日志到数据库
try {
sysLogService.save(new SysLog(userId, now, ip, "退出", "退出", method, url, null, ConstantUtil.SUCCESS,
null, null, null));
} catch (Exception e) {
log.error("记录操作日志失败", e);
}
}
private void handleLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {
resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
out.write(jsonMapper.writeValueAsString(Response.ok("退出成功! ")));
out.flush();
out.close();
}
}
- 在Security配置类
configure(HttpSecurity http)
方法中重写Filter替换UsernamePasswordAuthenticationFilter
,从JSON中获取用户名密码进行验证。这里我们添加两种验证方式:jsonUsernamePasswordAuthenticationFilter()
和zwxtLoginAuthenticationFilter()
。- 在自定义Filter中为Spring Security配置一个需要身份验证的请求匹配器,它使用
AntPathRequestMatcher
来匹配请求的URL路径,如果请求的URL路径与"/user/login"相匹配,并且HTTP方法为POST,那么该请求将被要求进行身份验证。- 设置自定义
AuthenticationManager
自定义认证方法。
2.创建自定义FilterJsonUsernamePasswordAuthenticationFilter
拦截器
java
public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final String USER_PARAMETER = "user";
private final JsonMapper jsonMapper;
private final UserLoginPwdErrorService userLoginPwdErrorService;
public JsonUsernamePasswordAuthenticationFilter(JsonMapper jsonMapper, UserLoginPwdErrorService userLoginPwdErrorService) {
this.jsonMapper = jsonMapper;
this.userLoginPwdErrorService = userLoginPwdErrorService;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 从JSON中读取用户名和密码
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
String username = null;
String password = null;
try (InputStream is = request.getInputStream()) {
// 将传入的JSON数据转换成Map,然后就可以通过get("key")获取
Map<String, String> authenticationBean = jsonMapper.readValue(is, new TypeReference<Map<String, String>>() {
});
AES aes = SecureUtil.aes("publicinstitutio".getBytes());
String usernameAndPassword = aes.decryptStr(authenticationBean.get(USER_PARAMETER));
int index = usernameAndPassword.indexOf(",");
username = usernameAndPassword.substring(0, index);
password = usernameAndPassword.substring(index + 1);
} catch (IOException e) {
logger.error("", e);
}
username = (username != null) ? username : "";
username = username.trim();
password = (password != null) ? password : "";
//根据用户名检查最近密码连续输入错误次数是否超过配置最大值
userLoginPwdErrorService.check(username);
SysUserDetails sysUserDetails = new SysUserDetails(username);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUserDetails, password);
//sessionRegistry.registerNewSession(request.getSession().getId(), authRequest.getPrincipal())
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
// 不是,就使用原来父类的key-value形式获取
return super.attemptAuthentication(request, response);
}
}
3.创建自定义FilterZwxtLoginAuthenticationFilter
拦截器
java
@Slf4j
public class ZwxtLoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final String CODE_PARAMETER = "code";
private static final String TIMESTAMP_PARAMETER = "timestamp";
private static final String SIGN_PARAMETER = "sign";
private final JsonMapper jsonMapper;
private final SipacZwxtService sipacZwxtService;
public ZwxtLoginAuthenticationFilter(JsonMapper jsonMapper, SipacZwxtService sipacZwxtService) {
this.jsonMapper = jsonMapper;
this.sipacZwxtService = sipacZwxtService;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 从JSON中读取code、timestamp、sign
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
String code = null;
String timestamp = null;
String sign = null;
try (InputStream is = request.getInputStream()) {
// 将传入的JSON数据转换成Map,然后就可以通过get("key")获取
Map<String, String> authenticationBean = jsonMapper.readValue(is, new TypeReference<Map<String, String>>() {
});
code = authenticationBean.get(CODE_PARAMETER);
timestamp = authenticationBean.get(TIMESTAMP_PARAMETER);
sign = authenticationBean.get(SIGN_PARAMETER);
} catch (IOException e) {
this.logger.error(e);
}
this.logger.info("zw系统传递的授权码code为:" + code + ",时间戳timestamp为:" + timestamp + ",签名sign为:" + sign);
if (!StringUtils.hasLength(code)) {
this.logger.error("zw系统传递的授权码code为空!");
throw new AuthenticationServiceException("zw系统传递的授权码code为空!");
}
if (!StringUtils.hasLength(timestamp)) {
this.logger.error("zw系统传递的时间戳timestamp为空!");
throw new AuthenticationServiceException("zw系统传递的时间戳timestamp为空!");
}
if (!StringUtils.hasLength(sign)) {
this.logger.error("zw系统传递的签名sign为空!");
throw new AuthenticationServiceException("zw系统传递的签名sign为空!");
}
if (!sipacZwxtService.validateSign(code, Long.parseLong(timestamp), sign)) {
this.logger.error("zw系统传递的签名sign验证未通过,授权码code无效!");
throw new AuthenticationServiceException("zw系统传递的签名sign验证未通过,授权码code无效!");
}
ThirdOauthResult thirdOauthResult;
try {
thirdOauthResult = sipacZwxtService.thirdOauth(code);
} catch (Exception e) {
this.logger.error("调用zw系统认证中心接口出现异常! " + e.getMessage(), e);
throw new AuthenticationServiceException("调用zw系统认证中心接口出现异常! " + e.getMessage(), e);
}
//认证失败
if (thirdOauthResult.getCode() != 0) {
this.logger.error("调用zw系统认证中心接口进行认证未通过,具体信息为: " + thirdOauthResult.getMsg());
throw new SipacZwxtValidateException(thirdOauthResult.getMsg());
}
if (thirdOauthResult.getObj() == null) {
this.logger.error("调用zw系统认证中心接口进行认证结果内容为空,无法取得accountName字段");
throw new SipacZwxtValidateException("认证结果内容为空,无法取得accountName字段");
}
String userId = thirdOauthResult.getObj().getAccountName();
if (!StringUtils.hasLength(userId)) {
this.logger.error("调用zw系统认证中心接口进行认证结果内容中accountName字段为空");
throw new SipacZwxtValidateException("认证结果内容中accountName字段为空");
}
SysUserDetails sysUserDetails = new SysUserDetails(userId);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUserDetails, null);
//sessionRegistry.registerNewSession(request.getSession().getId(), authRequest.getPrincipal())
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
// 不是,就使用原来父类的key-value形式获取
return super.attemptAuthentication(request, response);
}
}