概要
接口跳转源码分析
springsecurity6中http://localhost:8080/test/toLogin?username=lkz&pwd=123456的源码跟踪
相关代码
controller层
package com.lkz.controller;
import com.lkz.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class SpringSecurityController {
@Autowired
UserService userService;
@RequestMapping({"/index","/"})
public String index(int id){
System.out.println(userService.selectPermsById(id));
System.out.println(userService.selectNameById(id));
return "index";
}
@RequestMapping("/toLogin")
public String toLogin(@RequestParam("username") String username, @RequestParam("pwd") String pwd){
String login = userService.login(username, pwd);
return "OK";
}
@RequestMapping("/level1/{a}")
public String toLevel1(@PathVariable("a") String a){
return "views/level1/"+a;
}
@RequestMapping("/level2/{b}")
public String toLevel2(@PathVariable("b") String b){
return "views/level2/"+b;
}
@RequestMapping("/level3/{c}")
public String toLevel3(@PathVariable("c") String c){
return "views/level3/"+c;
}
}
service层
package com.lkz.service;
import com.lkz.mapper.UserMapper;
import com.lkz.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
public interface UserService {
public String selectPermsById(int id);
String selectNameById(@Param("id") int id);
String selectPwdById(@Param("id") int id);
User selectUser(@Param("username")String name);
//登陆逻辑
String login(String username,String pwd);
}
serviceimpl
package com.lkz.service.impl;
import com.lkz.mapper.UserMapper;
import com.lkz.pojo.User;
import com.lkz.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
private final AuthenticationManager authenticationManager;
public UserServiceImpl(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public String selectPermsById(int id) {
String perms = userMapper.selectPermsById(id);
return perms;
}
@Override
public String selectNameById(int id) {
String username = userMapper.selectNameById(id);
return username;
}
public String selectPwdById(int id) {
String pwd = userMapper.selectPwdById(id);
return pwd;
}
public User selectUser(String name) {
User user= userMapper.selectUser(name);
return user;
}
public String login(String username,String pwd){
//传入用户名和密码
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, pwd);
//实现登陆逻辑,此时就会去调用loadUserByUsername
//authenticate其实就是UserDetails(authentication前台拿到的user对象)
Authentication authenticate = authenticationManager.authenticate(authentication);
log.info("authenticate========="+authenticate);
return "OKKKKK";
}
}
userDetailserviceimpl获得数据库对象的方法实现类
package com.lkz.service.impl;
import com.lkz.mapper.UserMapper;
import com.lkz.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* UserDetails:得出库里的数据
*/
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectUser(username);
if (user == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
log.info("user======"+user.getPwd());
List<GrantedAuthority> atuhs = AuthorityUtils.commaSeparatedStringToAuthorityList(user.getPerms());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPwd(),
atuhs
/* Add user roles/authorities here */);
}
}
源码跳转
核心主要是从login处的源码追踪
所涉及到的主要方法:
- login:controller跳转进入的登陆逻辑方法
- authenticate:login方法中总的认证入口方法
- provider.authenticate(authentication):拿着前台传入的用户名密码对象authentication进行认证
- retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication):检索user对象【打算调用数据库查的方法了,所以必须要传入用户名,以及前台对象authentication】
- this.getUserDetailsService().loadUserByUsername(username):计划调用自写的实现类的方法调数据库查user对象
- additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication):将user对象【库里的】和前台接收到组合后的待比对user对象【authentication】
- this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())):进入比对方法
- BCrypt.checkpw(rawPassword.toString(), encodedPassword):将前台明文和库里密文传入,获取盐进行加密比对【equalsNoEarlyReturn】
- equalsNoEarlyReturn(hashed, hashpwforcheck(passwordb, hashed)):将库里取出的密文和前台接受到的明文加密后比对
源码中的核心代码
authenticate:login方法中总的认证入口方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// 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 ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.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 then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
provider.authenticate(authentication):拿着前台传入的用户名密码对象authentication进行认证 AbstractUserDetailsAuthenticationProvider实现类的
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication):检索user对象【打算调用数据库查的方法了,所以必须要传入用户名,以及前台对象authentication】
@Override
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);
}
}
返回到AbstractUserDetailsAuthenticationProvider实现类中,继续执行additionalAuthenticationChecks
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
AbstractUserDetailsAuthenticationProvider抽象类-->DaoAuthenticationProvider实现类中的additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken 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();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
matches核心方法,调PasswordEncoder接口--->BCryptPasswordEncoder实现类的
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("Empty encoded password");
return false;
}
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
BCrypt.checkpw
public static boolean checkpw(String plaintext, String hashed) {
byte[] passwordb = plaintext.getBytes(StandardCharsets.UTF_8);
return equalsNoEarlyReturn(hashed, hashpwforcheck(passwordb, hashed));
}
equalsNoEarlyReturn
static boolean equalsNoEarlyReturn(String a, String b) {
return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
}
最后会执行到login中总的认证方法中,result = provider.authenticate(authentication);不为空,返回的是result对象
认证成功
后台会有显示已认证
小结
核心就是比对密码,要注意数据库中放密文