|
| 1 | +Spring Security的登录主要是由一系列的过滤器组成,我们如果需要修改登录的校验逻辑,只需要在过滤器链路上添加修改相关的逻辑即可。这里主要通过Spring Security的源码来了解相关的认证登录的逻辑。 |
| 2 | + |
| 3 | +#### 1.Spring Security的认证流程 |
| 4 | + |
| 5 | +主要分析: |
| 6 | + |
| 7 | +1. 认证用户的流程 |
| 8 | +2. 如何进行认证校验 |
| 9 | +3. 认证成功后怎么获取用户信息 |
| 10 | + |
| 11 | +具体的过滤器链路如下所示: |
| 12 | + |
| 13 | +[](https://imgtu.com/i/cT2G4g) |
| 14 | + |
| 15 | +Spring Security的认证流程图如下,认证的主要过程有: |
| 16 | + |
| 17 | +1. 用户提交用户名和密码,然后通过UsernamePasswordAuthenticationFilter对其进行封装成为UsernamePasswordAuthenticationToken对象,这个是AbstractAuthenticationToken的子类,而AbstractAuthenticationToken又是Authentication的一个实现,所以可以看到后续获取的都是Authentication类型的对象实例; |
| 18 | +2. 将第一步的UsernamePasswordAuthenticationToken对象传递给AuthenticationManager; |
| 19 | +3. 通过AbstractUserDetailsAuthenticationProvider的默认实现类DaoAuthenticationProvider的retrieveUser方法,这个方法会调用UserDetailsService的loadUserByUsername方法来进行用户名和密码的判断,使用的默认的逻辑进行处理; |
| 20 | +4. 将成功认证后的用户信息放入到SecurityContextHolder中,之后可以通过SecurityContext获取用户的相关信息。 |
| 21 | + |
| 22 | +[](https://imgtu.com/i/coGpvR) |
| 23 | + |
| 24 | +spring-security源码下载地址: |
| 25 | + |
| 26 | +```java |
| 27 | +https://github.com/spring-projects/spring-security |
| 28 | +``` |
| 29 | + |
| 30 | +#### 2.Spring Security的认证源码分析 |
| 31 | + |
| 32 | +##### 2.1 搭建项目并访问 |
| 33 | + |
| 34 | +首先我们搭建一个Spring Security的项目,使用Spring Boot可以很方便的进行集成开发,主要引入如下的依赖即可(当然也可以查看官网,选择合适的版本): |
| 35 | + |
| 36 | +```java |
| 37 | +<dependency> |
| 38 | + <groupId>org.springframework.boot</groupId> |
| 39 | + <artifactId>spring-boot-starter-security</artifactId> |
| 40 | +</dependency> |
| 41 | +``` |
| 42 | + |
| 43 | +启动项目后会随机生成一个密码串,这里需要复制保存以便登录的时候使用: |
| 44 | + |
| 45 | +[](https://imgtu.com/i/coJ0ld) |
| 46 | + |
| 47 | +访问登录地址: |
| 48 | + |
| 49 | +```java |
| 50 | +http://localhost:8080/login |
| 51 | +``` |
| 52 | + |
| 53 | +[](https://imgtu.com/i/coJfpQ) |
| 54 | + |
| 55 | +默认的账户名和密码: |
| 56 | + |
| 57 | +```java |
| 58 | +账户名: user |
| 59 | +密码: 项目启动时生成的密码串 |
| 60 | +``` |
| 61 | + |
| 62 | +##### 2.2 进行源码分析 |
| 63 | + |
| 64 | +1. 进行断点后会发现首先进入的是UsernamePasswordAuthenticationFilter的attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法,会对用户名和密码进行封装成UsernamePasswordAuthenticationToken对象,然后调用this.getAuthenticationManager().authenticate(authRequest)方法进入到AuthenticationManager中。 |
| 65 | + |
| 66 | + attemptAuthentication方法源码如下所示: |
| 67 | + |
| 68 | +```java |
| 69 | +@Override |
| 70 | + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) |
| 71 | + throws AuthenticationException { |
| 72 | + if (this.postOnly && !request.getMethod().equals("POST")) { |
| 73 | + throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); |
| 74 | + } |
| 75 | + String username = obtainUsername(request); |
| 76 | + username = (username != null) ? username : ""; |
| 77 | + username = username.trim(); |
| 78 | + String password = obtainPassword(request); |
| 79 | + password = (password != null) ? password : ""; |
| 80 | + UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); |
| 81 | + // Allow subclasses to set the "details" property |
| 82 | + setDetails(request, authRequest); |
| 83 | + return this.getAuthenticationManager().authenticate(authRequest); |
| 84 | + } |
| 85 | +``` |
| 86 | + |
| 87 | +2. 随后请求进入到WebSecurityConfigurerAdapter的AuthenticationManagerDelegator中,AuthenticationManagerDelegator是AuthenticationManager的一个子类,最后封装成为UsernamePasswordAuthenticationToken对象,供DaoAuthenticationProvider使用。 |
| 88 | + |
| 89 | + AuthenticationManagerDelegator的源码如下: |
| 90 | + |
| 91 | + ```java |
| 92 | + static final class AuthenticationManagerDelegator implements AuthenticationManager { |
| 93 | + private AuthenticationManagerBuilder delegateBuilder; |
| 94 | + private AuthenticationManager delegate; |
| 95 | + private final Object delegateMonitor = new Object(); |
| 96 | + private Set<String> beanNames; |
| 97 | + |
| 98 | + AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder, ApplicationContext context) { |
| 99 | + Assert.notNull(delegateBuilder, "delegateBuilder cannot be null"); |
| 100 | + Field parentAuthMgrField = ReflectionUtils.findField(AuthenticationManagerBuilder.class, "parentAuthenticationManager"); |
| 101 | + ReflectionUtils.makeAccessible(parentAuthMgrField); |
| 102 | + this.beanNames = getAuthenticationManagerBeanNames(context); |
| 103 | + validateBeanCycle(ReflectionUtils.getField(parentAuthMgrField, delegateBuilder), this.beanNames); |
| 104 | + this.delegateBuilder = delegateBuilder; |
| 105 | + } |
| 106 | + |
| 107 | + public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
| 108 | + if (this.delegate != null) { |
| 109 | + return this.delegate.authenticate(authentication); |
| 110 | + } else { |
| 111 | + synchronized(this.delegateMonitor) { |
| 112 | + if (this.delegate == null) { |
| 113 | + this.delegate = (AuthenticationManager)this.delegateBuilder.getObject(); |
| 114 | + this.delegateBuilder = null; |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + return this.delegate.authenticate(authentication); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + private static Set<String> getAuthenticationManagerBeanNames(ApplicationContext applicationContext) { |
| 123 | + String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, AuthenticationManager.class); |
| 124 | + return new HashSet(Arrays.asList(beanNamesForType)); |
| 125 | + } |
| 126 | + |
| 127 | + private static void validateBeanCycle(Object auth, Set<String> beanNames) { |
| 128 | + if (auth != null && !beanNames.isEmpty() && auth instanceof Advised) { |
| 129 | + TargetSource targetSource = ((Advised)auth).getTargetSource(); |
| 130 | + if (targetSource instanceof LazyInitTargetSource) { |
| 131 | + LazyInitTargetSource lits = (LazyInitTargetSource)targetSource; |
| 132 | + if (beanNames.contains(lits.getTargetBeanName())) { |
| 133 | + throw new FatalBeanException("A dependency cycle was detected when trying to resolve the AuthenticationManager. Please ensure you have configured authentication."); |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | + ``` |
| 140 | + |
| 141 | + org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration.AuthenticationManagerDelegator#authenticate |
| 142 | + |
| 143 | + ```java |
| 144 | + @Override |
| 145 | + public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
| 146 | + if (this.delegate != null) { |
| 147 | + return this.delegate.authenticate(authentication); |
| 148 | + } |
| 149 | + synchronized (this.delegateMonitor) { |
| 150 | + if (this.delegate == null) { |
| 151 | + this.delegate = this.delegateBuilder.getObject(); |
| 152 | + this.delegateBuilder = null; |
| 153 | + } |
| 154 | + } |
| 155 | + return this.delegate.authenticate(authentication); |
| 156 | + } |
| 157 | + ``` |
| 158 | + |
| 159 | +3. 进入到DaoAuthenticationProvider的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法进行用户的认证,这里的认证主要会调用默认的UserDetailsService对用户名和密码进行校验,如果是使用的类似于Mysql的数据源,其默认的实现是JdbcDaoImpl。 |
| 160 | + |
| 161 | + org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser |
| 162 | + |
| 163 | + ```java |
| 164 | + @Override |
| 165 | + protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) |
| 166 | + throws AuthenticationException { |
| 167 | + prepareTimingAttackProtection(); |
| 168 | + try { |
| 169 | + UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); |
| 170 | + if (loadedUser == null) { |
| 171 | + throw new InternalAuthenticationServiceException( |
| 172 | + "UserDetailsService returned null, which is an interface contract violation"); |
| 173 | + } |
| 174 | + return loadedUser; |
| 175 | + } |
| 176 | + catch (UsernameNotFoundException ex) { |
| 177 | + mitigateAgainstTimingAttack(authentication); |
| 178 | + throw ex; |
| 179 | + } |
| 180 | + catch (InternalAuthenticationServiceException ex) { |
| 181 | + throw ex; |
| 182 | + } |
| 183 | + catch (Exception ex) { |
| 184 | + throw new InternalAuthenticationServiceException(ex.getMessage(), ex); |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + ``` |
| 189 | + |
| 190 | +4. 将上一步认证后的用户实例放入SecurityContextHolder中,至此我们可以很方便的从SecurityContextHolder中获取用户信息,方法如下: |
| 191 | + |
| 192 | + ```java |
| 193 | + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
| 194 | + ``` |
| 195 | + |
| 196 | + |
0 commit comments