当搭建好security授权服务器,资源服务器,和单点登录客户端的时候,挺开心的,基本上都是使用注解写的,也没啥多大的变动.
但是在我使用ResponseBodyAdvice包装类统一返回响应请求的时候,问题就来了,再进行统一包装的时候也会将通过授权后返回的token信息进行包装,可是当客户端对返回的response进行解析的时候死活解析不出来,会把返回的信息全部放入到增强内容additionallnformation当中
这就令我很苦恼,也算没吃透security就开始写带来的问题,找了很多方法,但是都没啥意义.
主要我的客户端是使用security提供的默认的配置,都不好进行改动,而自己又不想去另写客户端(这样还要去写单点登录).
解决的方法其实也有
1.配置security中的响应请求进行重写,然后放入到security config配置中(写了,但由于是使用默认配置,报错了 bean冲突)
2.删掉统一包装方法(暂时使用,因为反正是授权,不包装也可以)
3.跟1其实差不多,别使用默认的注解配置,自己去配置客户端,写sso代码,这样就不用局限于默认框架的拘束之下了.
Security身份认证顺序问题
另一个问题,当用户被封禁后isEnabled()方法为false的时候,用户isAccountNonLocked()同时被触发时,这里就会提前返回用户账户被锁定,源码中首先校验的是是否锁定,在AbstractUserDetailsAuthenticationProvider类中有默认的校验方案
这就会让用户接受到不正确的信息,而这正是默认实现的DaoAuthenticationProvider实现的校验策略,所以我们要用自己的AuthenticationProvider,并且去自己实现用户身份校验.
AuthenticationSuccessEvent无法监听问题
今天又遇到一个问题,监听类收不到广播类推送的登录成功消息,先放出代码.
/**
* 用户登录事件监听
* 如果用户登录成功则在日志进行记录
* 并且发送邮件告知用户登录,以及登录位置
* 提醒end user账号被登录
*/
@Component
public class UserLoginListener {
@Autowired
private HttpServletRequest request;
@Autowired
private EmailUtil emailUtil;
@EventListener(AuthenticationSuccessEvent.class)
public void userLoginSuccessfulOnApplicationEvent(AuthenticationSuccessEvent event) throws MessagingException {
//根据访问的请求ip查找用户的位置
//http://whois.pconline.com.cn/?ip=123.91.217.123查询IP地址的地理位置
String url = "http://whois.pconline.com.cn/?ip=" + this.getIpAddress();
String result = this.getResult(url);
int data = result.indexOf("位置:");
int first = data + 3;
int last = data + 20;
String geographicLocation = result.substring(first, last).trim();
System.out.println(geographicLocation);
//拿到登录的用户信息
Authentication authentication = event.getAuthentication();
Object principal = authentication.getPrincipal();
String userName = authentication.getName();
String userEmail = null;
//获取电子邮箱
if (principal instanceof User) {
User user = (User) principal;
userEmail = user.getEmail();
}
String userRole = null;
String bodyMessage = authentication.getName() + " 您好,您刚刚在 " + geographicLocation + "\n登录,如果这不是您" +
"本人进行的操作,请赶紧修改密码或者冻结账号";
Set<String> authorizeds = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
for (String authorized : authorizeds) {
if (authorized.equals("normal")) {
userRole = "尊敬的用户 ";
break;
} else if (authorized.equals("root")) {
userRole = "超级管理员管理员SAMA ";
} else if (authorized.equals("admin")) {
userRole = "管理员SAMA ";
} else if (authorized.equals("CLIENT")) {
userRole = "Sso客户端信息 \n";
bodyMessage = authentication.getName() + "--> 服务器启动了,Sso客户端正在上线中";
}
}
//邮件的提示信息
String emailMessage = userRole + bodyMessage;
String email = userEmail;
//发送邮件
Thread thread = new Thread(() -> emailUtil.loginUpEmail(userName, emailMessage, email));
thread.start();
}
监听的是AuthenticationSuccessEvent这个类,这个类很有意思,它的推送是由ProviderManager验证成功后推送的,之前修改了AuthenticationProvider,也没出现什么问题,但是今天使用的时候就开始整活,死活都监听不到消息,估计就是没推送,换其他事件都能监听到,直接打断点到ProviderManager,可以看到.
private AuthenticationEventPublisher eventPublisher;
...............
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException var12) {
} catch (AuthenticationException var13) {
parentException = var13;
lastException = var13;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
//Manager自己实现的推送方法
private static final class NullEventPublisher implements AuthenticationEventPublisher {
private NullEventPublisher() {
}
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
}
public void publishAuthenticationSuccess(Authentication authentication) {
}
}
看完就明白了,啥玩意,搞个空的等于就推了空气,所以只需要自己在验证成功后自己手工去推一下就好了.
**
* 自定义认证条件
*/
@Component
public class UserAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
protected final Log logger = LogFactory.getLog(this.getClass());
@Autowired
private UserService authUserDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private ApplicationEventPublisher publisher;
/**
* @Description 认证处理,返回一个Authentication的实现类则代表认证成功,返回null则代表认证失败
* @Date 2019/7/5 15:19
* @Version 1.0
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//判断访问权限是否为空
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
//创建UserDetails类
UserDetails user = authUserDetailsService.loadUserByUsername(authentication.getName());
//判断密码是否正确,如果不正确则抛出异常
if (!this.passwordEncoder.matches(presentedPassword, user.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
//判断用户身份
if (!user.isEnabled()) {
this.logger.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
} else if (!user.isAccountNonLocked()) {
this.logger.debug("Failed to authenticate since user account is locked");
throw new LockedException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
} else if (!user.isAccountNonExpired()) {
this.logger.debug("Failed to authenticate since user account has expired");
throw new AccountExpiredException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
//获取用户权限信息
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
/**
* 一定要记住,如果没收到消息,就要自己实现,因为在ProviderManager里面它不会给你实现,而是使用
* 自己空的方法去发布,等于放了个屁,然后就接收不到验证成功的信息,所以要自己实现一个
*/
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, presentedPassword, authorities);
publisher.publishEvent(new AuthenticationSuccessEvent(usernamePasswordAuthenticationToken));
return usernamePasswordAuthenticationToken;
}
/**
* @Description 如果该AuthenticationProvider支持传入的Authentication对象,则返回true
* @Date 2019/7/5 15:18
* @Version 1.0
*/
@Override
public boolean supports(Class<?> aClass) {
return aClass.equals(UsernamePasswordAuthenticationToken.class);
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
}
而且要注意,它接收的参数是Authentication authentication,可别直接就把一开始拿到的就放进去,那个只是身份验证,不能直接传进去,否则只能拿到账号和密码,必须要创建一个包装的方法,同时将其他的信息放入.