一、默认登录页面

在这里插入图片描述

  1. 请求 /hello 接口,在引入 spring security 之后会先经过一些列过滤器
  2. 在请求到达 FilterSecurityInterceptor时,发现请求并未认证。请求拦截下来,并抛出 AccessDeniedException 异常。
  3. 抛出 AccessDeniedException 的异常会被 ExceptionTranslationFilter 捕获,这个 Filter 中会调用 LoginUrlAuthenticationEntryPoint#commence 方法给客户端返回302,要求客户端进行重定向到 /login 页面。
  4. 客户端发送 /login 请求。
  5. /login 请求会再次被拦截器中 DefaultLoginPageGeneratingFilter 拦截到,并在拦截器中返回生成登录页面。
//DefaultLoginPageGeneratingFilter.generateLoginPageHtml()
 private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
        String errorMsg = "Invalid credentials";
        if (loginError) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                AuthenticationException ex = (AuthenticationException)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
                errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";
            }
        }
        String contextPath = request.getContextPath();
        StringBuilder sb = new StringBuilder();
        sb.append("<!DOCTYPE html>\n");
        sb.append("<html lang=\"en\">\n");
        sb.append("  <head>\n");
        sb.append("    <meta charset=\"utf-8\">\n");
        sb.append("    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
        sb.append("    <meta name=\"description\" content=\"\">\n");
        sb.append("    <meta name=\"author\" content=\"\">\n");
        sb.append("    <title>Please sign in</title>\n");
        sb.append("    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n");
        sb.append("    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n");
        sb.append("  </head>\n");
        sb.append("  <body>\n");
        sb.append("     <div class=\"container\">\n");
        if (this.formLoginEnabled) {
            sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n");
            sb.append("        <h2 class=\"form-signin-heading\">Please sign in</h2>\n");
            sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "        <p>\n");
            sb.append("          <label for=\"username\" class=\"sr-only\">Username</label>\n");
            sb.append("          <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
            sb.append("        </p>\n");
            sb.append("        <p>\n");
            sb.append("          <label for=\"password\" class=\"sr-only\">Password</label>\n");
            sb.append("          <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n");
            sb.append("        </p>\n");
            sb.append(this.createRememberMe(this.rememberMeParameter) + this.renderHiddenInputs(request));
            sb.append("        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
            sb.append("      </form>\n");
        }

        if (this.openIdEnabled) {
            sb.append("      <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n");
            sb.append("        <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n");
            sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "        <p>\n");
            sb.append("          <label for=\"username\" class=\"sr-only\">Identity</label>\n");
            sb.append("          <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
            sb.append("        </p>\n");
            sb.append(this.createRememberMe(this.openIDrememberMeParameter) + this.renderHiddenInputs(request));
            sb.append("        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
            sb.append("      </form>\n");
        }

        Iterator var7;
        Entry relyingPartyUrlToName;
        String url;
        String partyName;
        if (this.oauth2LoginEnabled) {
            sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
            sb.append(createError(loginError, errorMsg));
            sb.append(createLogoutSuccess(logoutSuccess));
            sb.append("<table class=\"table table-striped\">\n");
            var7 = this.oauth2AuthenticationUrlToClientName.entrySet().iterator();

            while(var7.hasNext()) {
                relyingPartyUrlToName = (Entry)var7.next();
                sb.append(" <tr><td>");
                url = (String)relyingPartyUrlToName.getKey();
                sb.append("<a href=\"").append(contextPath).append(url).append("\">");
                partyName = HtmlUtils.htmlEscape((String)relyingPartyUrlToName.getValue());
                sb.append(partyName);
                sb.append("</a>");
                sb.append("</td></tr>\n");
            }

            sb.append("</table>\n");
        }

        if (this.saml2LoginEnabled) {
            sb.append("<h2 class=\"form-signin-heading\">Login with SAML 2.0</h2>");
            sb.append(createError(loginError, errorMsg));
            sb.append(createLogoutSuccess(logoutSuccess));
            sb.append("<table class=\"table table-striped\">\n");
            var7 = this.saml2AuthenticationUrlToProviderName.entrySet().iterator();

            while(var7.hasNext()) {
                relyingPartyUrlToName = (Entry)var7.next();
                sb.append(" <tr><td>");
                url = (String)relyingPartyUrlToName.getKey();
                sb.append("<a href=\"").append(contextPath).append(url).append("\">");
                partyName = HtmlUtils.htmlEscape((String)relyingPartyUrlToName.getValue());
                sb.append(partyName);
                sb.append("</a>");
                sb.append("</td></tr>\n");
            }

            sb.append("</table>\n");
        }

        sb.append("</div>\n");
        sb.append("</body></html>");
        return sb.toString();
    }

    private String renderHiddenInputs(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        Iterator var3 = ((Map)this.resolveHiddenInputs.apply(request)).entrySet().iterator();

        while(var3.hasNext()) {
            Entry<String, String> input = (Entry)var3.next();
            sb.append("<input name=\"");
            sb.append((String)input.getKey());
            sb.append("\" type=\"hidden\" value=\"");
            sb.append((String)input.getValue());
            sb.append("\" />\n");
        }
        return sb.toString();
    }

就是通过这种方式,Spring Security 默认过滤器中生成了登录页面,并返回!

二、默认登录用户

  1. 查看 SpringBootWebSecurityConfiguration.defaultSecurityFilterChain 方法表单登录
    在这里插入图片描述
  2. 处理登录为 FormLoginConfigurer 类中 调用 这个类实例
    在这里插入图片描述
  3. 查看类中 UsernamePasswordAuthenticationFilter#attempAuthentication 方法得知实际调用 AuthenticationManager 中 authenticate 方法
    在这里插入图片描述
  4. 调用 ProviderManager 类中方法 authenticate

在这里插入图片描述

  1. 调用了 ProviderManager 实现类中 AbstractUserDetailsAuthenticationProvider类中方法

在这里插入图片描述

  1. 最终调用实现类 DaoAuthenticationProvider 类中方法比较

在这里插入图片描述

在这里插入图片描述

看到这里就知道默认实现是基于 InMemoryUserDetailsManager 这个类,也就是内存的实现!

三、自定义用户数据源

  1. InMemoryUserDetailsManager

看一下InMemoryUserDetailsManager的继承图

在这里插入图片描述
可以看到顶层是UserDetailService接口,接口中 loadUserByUserName 方法是用来在认证时进行用户名认证方法,默认实现使用是内存实现,如果想要修改数据库实现我们只需要自定义 UserDetailService 实现,最终返回 UserDetails 实例即可。

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

在这里插入图片描述

在第一篇文章中,我们知道,springboot会基于SPI机制,导入一个UserDetailServiceAutoConfigutation,用于对UserDetailService进行配置。

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({AuthenticationManager.class})
@ConditionalOnBean({ObjectPostProcessor.class})
@ConditionalOnMissingBean(
    value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class},
    type = {"org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"}
)
public class UserDetailsServiceAutoConfiguration {
    private static final String NOOP_PASSWORD_PREFIX = "{noop}";
    private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
    private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

    public UserDetailsServiceAutoConfiguration() {
    }

    @Bean
    @Lazy
    //默认是InMemoryUserDetailsManager 
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
        User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
    }

    private String getOrDeducePassword(User user, PasswordEncoder encoder) {
        String password = user.getPassword();
        if (user.isPasswordGenerated()) {
            logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
        }

        return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
    }
}
  • 结论
  1. 从自动配置源码中得知当 classpath 下存在 AuthenticationManager 类
  2. 当前项目中,系统没有提供 AuthenticationManager.class、 AuthenticationProvider.class、UserDetailsService.class、 AuthenticationManagerResolver.class、实例

默认情况下都会满足,此时Spring Security会提供一个 InMemoryUserDetailManager 实例

在这里插入图片描述

@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
	private final User user = new User();
	public User getUser() {
		return this.user;
  }
  //....
	public static class User {
		private String name = "user";
		private String password = UUID.randomUUID().toString();
		private List<String> roles = new ArrayList<>();
		private boolean passwordGenerated = true;
		//get set ...
	}
}

这就是默认生成 user 以及 uuid 密码过程! 另外看明白源码之后,就知道只要在配置文件中加入如下配置可以对内存中用户和密码进行覆盖。

spring.security.user.name=root
spring.security.user.password=root
spring.security.user.roles=admin,users
  1. 自定义数据源
  • 自定义内存数据源:没什么意义
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager inMemoryUserDetailsManager
                = new InMemoryUserDetailsManager();
        UserDetails u1 = User.withUsername("zhangs")
                .password("{noop}111").roles("USER").build();
        inMemoryUserDetailsManager.createUser(u1);
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) 
      throws Exception {
        auth.userDetailsService(userDetailsService());
    }  	
}
  • 自定义数据库数据源

对象实现UserDetails 接口

public class User  implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    private List<Role> roles = new ArrayList<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
        return grantedAuthorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
		//get/set....
}

Role 类

public class Role {
    private Integer id;
    private String name;
    private String nameZh;
  	//get set..
}

创建 UserDao 接口

@Mapper
public interface UserDao {
    //根据用户名查询用户
    User loadUserByUsername(String username);
  	
  	//根据用户id查询角色
  	List<Role> getRolesByUid(Integer uid);
}
创建 UserDetailService 实例
@Component
public class MyUserDetailService implements UserDetailsService {

    private  final UserDao userDao;

    @Autowired
    public MyUserDetailService(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.loadUserByUsername(username);
        if(ObjectUtils.isEmpty(user))throw new RuntimeException("用户不存在");
        user.setRoles(userDao.getRolesByUid(user.getId()));
        return user;
    }
}

配置 authenticationManager 使用自定义UserDetailService

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  
    private final UserDetailsService userDetailsService;

    @Autowired
    public WebSecurityConfigurer(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userDetailsService);
    }
  	@Override
    protected void configure(HttpSecurity http) throws Exception {
      //web security..
    }
}

数据库表和mapper.xml按要求设计即可

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐