若依封装SpringSecurity+JWT
公司用的是若依管理系统封装了SpringBoot,权限使用了SpringSecurity+JWT令牌机制,今天花时间研究了一下若依是如何对Security进行封装的。登录1.用户存在性验证首先将后台清了一下,访问系统登录的页面,发现控制台打印了如下SQL:16:10:13.119 [http-nio-8082-exec-15] DEBUG c.r.s.m.S.selectUserByUserNam
公司用的是若依管理系统封装了SpringBoot,权限使用了SpringSecurity+JWT令牌机制,今天花时间研究了一下若依是如何对Security进行封装的。
登录
1.用户存在性验证
首先将后台清了一下,访问系统登录的页面,发现控制台打印了如下SQL:
16:10:13.119 [http-nio-8082-exec-15] DEBUG c.r.s.m.S.selectUserByUserName - [debug,159] - ==> Preparing: select u.user_id, u.dept_id, u.user_name, u.nick_name, u.user_type, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, u.entry_time, u.staff_type, u.salary_base, u.is_calc, d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status from sys_user u left join sys_dept d on u.dept_id = d.dept_id left join sys_user_role ur on u.user_id = ur.user_id left join sys_role r on r.role_id = ur.role_id where u.user_name = ?
16:10:13.120 [http-nio-8082-exec-15] DEBUG c.r.s.m.S.selectUserByUserName - [debug,159] - ==> Parameters: admin(String)
16:10:13.123 [http-nio-8082-exec-15] DEBUG c.r.s.m.S.selectUserByUserName - [debug,159] - <== Total: 1
16:10:13.246 [schedule-pool-3] INFO sys-user - [run,54] - [192.168.*.***]内网IP[admin][Success][登录成功]
16:10:13.246 [schedule-pool-3] DEBUG c.r.s.m.S.insertLogininfor - [debug,159] - ==> Preparing: insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) values (?, ?, ?, ?, ?, ?, ?, sysdate())
16:10:13.247 [schedule-pool-3] DEBUG c.r.s.m.S.insertLogininfor - [debug,159] - ==> Parameters: admin(String), 0(String), 192.168.*.***(String), 内网IP(String), Chrome 8(String), Windows 10(String), 登录成功(String)
16:10:13.250 [schedule-pool-3] DEBUG c.r.s.m.S.insertLogininfor - [debug,159] - <== Updates: 1
16:10:13.297 [http-nio-8082-exec-19] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - ==> Preparing: select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_time from sys_menu m where m.menu_type in ('M', 'C') and m.status = 0 and m.visible = 0 order by m.parent_id, m.order_num
16:10:13.298 [http-nio-8082-exec-19] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - ==> Parameters:
16:10:13.304 [http-nio-8082-exec-19] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - <== Total: 128
16:10:13.486 [http-nio-8082-exec-22] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - ==> Preparing: select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_time from sys_menu m where m.menu_type in ('M', 'C') and m.status = 0 and m.visible = 0 order by m.parent_id, m.order_num
16:10:13.487 [http-nio-8082-exec-22] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - ==> Parameters:
16:10:13.491 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectRolesByUserName - [debug,159] - ==> Preparing: select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status, r.del_flag, r.create_time, r.remark from sys_role r left join sys_user_role ur on ur.role_id = r.role_id left join sys_user u on u.user_id = ur.user_id left join sys_dept d on u.dept_id = d.dept_id WHERE r.del_flag = '0' and u.user_name = ?
16:10:13.492 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectRolesByUserName - [debug,159] - ==> Parameters: admin(String)
16:10:13.495 [http-nio-8082-exec-22] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - <== Total: 128
16:10:13.499 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectRolesByUserName - [debug,159] - <== Total: 1
16:10:13.500 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectPostsByUserName - [debug,159] - ==> Preparing: select p.post_id, p.post_name, p.post_code from sys_post p left join sys_user_post up on up.post_id = p.post_id left join sys_user u on u.user_id = up.user_id where u.user_name = ?
16:10:13.501 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectPostsByUserName - [debug,159] - ==> Parameters: admin(String)
16:10:13.503 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectPostsByUserName - [debug,159] - <== Total: 1
一共6条sql语句,首先第一条查的是用户表的用户信息,于是就去看了一下数据库,将第一条sql又执行了一遍,结果如下:
可见密码是经过加密了,于是又去看login接口是怎么实现的,如下:
@Component
public class SysLoginController{
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
ajax.put(Constants.TOKEN, token);
return ajax;
}
}
可见核心方法在于 String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
于是开启断点追踪下去:loginService的login方法是这么实现的:
public class SysLoginService{
@Resource
private AuthenticationManager authenticationManager;
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @return 结果
*/
public String login(String username, String password){
String verifyKey = Constants.CAPTCHA_CODE_KEY;
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);
// if (captcha == null){
// AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
// throw new CaptchaExpireException();
// }
// if (!code.equalsIgnoreCase(captcha)){
// AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
// throw new CaptchaException();
// }
// 用户验证
Authentication authentication = null;
try{
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (Exception e){
if (e instanceof BadCredentialsException){
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new CustomException(e.getMessage());
}
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
}
}
注释的那部分是用于验证验证码的部分,断点继续往下走,登录的核心部分是,这也是SpringSecurity用于登录的核心方法.
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
关于AuthenticationManager,它是SpringSecurity的身份验证管理器,AuthenticationManager的authenticate()方法用于登录验证,其参数UsernamePasswordAuthenticationToken无疑是封装了用户登录信息的一个对象,于是现在开始进入到了SpringSecurity的环节,AuthenticationManager是一个接口,所以authenticate()方法必定交给它的实现类来完成,通过debug可以看见Spring为其注入的是WebSecurityConfigurerAdapter
那么问题来了,通过继承树发现WebSecurityConfigurerAdapter并非AuthenticationManager的实现类,那这就出事了,先不管,继续往下走
于是再进入authenticationManager.authenticate()方法中,调用者是WebSecurityConfigurerAdapter但是他有一个静态内部类AuthenticationManagerDelegator,而AuthenticationManagerDelegator实现了AuthenticationManager其实现的方法为
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
if (delegate != null) {
return delegate.authenticate(authentication);
}
synchronized (delegateMonitor) {
if (delegate == null) {
delegate = this.delegateBuilder.getObject();
this.delegateBuilder = null;
}
}
return delegate.authenticate(authentication);
}
这还是看不懂,继续走,于是再进入到delegate.authenticate(authentication);方法中,其实现如下:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
还是不懂,继续往里走,进入result = provider.authenticate(authentication);方法中(中间过程还有一步父子类的共同实现,但是是同一个方法的不同属性值,逻辑一样),如下:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
这仿佛一下子就开朗了,登录信息一直封装再参数authentication中,再次比对往下走,进入到了
retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);方法中,该方法的逻辑为
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
这一下子就出来了,UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);不正是去取数据库中的用户信息吗,且该处若出现异常,立即抛出UsernameNotFoundException,正对应上了之前的判断,在该断点处对该方法放行,果然控制台打印出了sql语句,真是登陆时的第一条sql,再次进入到该方法中,发现
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user)){
log.info("登录用户:{} 不存在.", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())){
log.info("登录用户:{} 已被删除.", username);
throw new BaseException("对不起,您的账号:" + username + " 已被删除");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus())){
log.info("登录用户:{} 已被停用.", username);
throw new BaseException("对不起,您的账号:" + username + " 已停用");
}
return createLoginUser(user);
}
这就一一对应上了
更多推荐
所有评论(0)