文章目录:
▶ 简介
▶ 框架搭建
▶ 环境要求
▶ 工程搭建
▶ 框架分析
▶ 跳转登录页面
▶ 用户名和密码
▶ 框架核心过滤器
▶ 框架核心组件
▶ Authentication
▶ SecurityContextHolder
▶ AuthenticationManager
▶ AuthenticationProvider
▶ UserDetailService
▶ GrantedAuthority
▶ 认证流程梳理
▶ SpringBoot整合Spring Security
▶ 资料下载地址
简介
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架(简单说是对访问权限进行控制 )。
它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IOC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
应用的安全性包括:用户认证(Authentication)和用户授权(Authorization)两个部分
用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统 。
-
用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程
用户授权:验证某个用户是否有权限执行某个操作
-
在一个系统中,不同用户所具有的权限是不同的
-
比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改
-
一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限
框架搭建
环境要求:
SpringBoot、SpringSecurity、MySQL
工程搭建:
-
创建一个SpringBoot项目,并引入相关包
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>2.1.5.RELEASE</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
21 <dependencies>
22 <dependency>
23 <groupId>org.springframework.boot</groupId>
24 <artifactId>spring-boot-starter-security</artifactId>
25 </dependency>
26 <dependency>
27 <groupId>org.springframework.boot</groupId>
28 <artifactId>spring-boot-starter-thymeleaf</artifactId>
29 </dependency>
30 <dependency>
31 <groupId>org.springframework.boot</groupId>
32 <artifactId>spring-boot-starter-web</artifactId>
33 </dependency>
34 <dependency>
35 <groupId>org.projectlombok</groupId>
36 <artifactId>lombok</artifactId>
37 <optional>true</optional>
38 </dependency>
39 <dependency>
40 <groupId>org.springframework.boot</groupId>
41 <artifactId>spring-boot-starter-test</artifactId>
42 <scope>test</scope>
43 </dependency>
44 <dependency>
45 <groupId>org.springframework.security</groupId>
46 <artifactId>spring-security-test</artifactId>
47 <scope>test</scope>
48 </dependency>
49 </dependencies>
构建启动类
1@SpringBootApplication
2public class SpringsecuritydemoApplication {
3
4 public static void main(String[] args) {
5 SpringApplication.run(SpringsecuritydemoApplication.class, args);
6 }
7
8}
启动项目,访问即可:http://localhost:8080
-
输入用户名和密码
-
-
用户名:user
-
密码:可以通过启动控制台查看
-
Using generated security password
-
框架分析
浏览器,访问http://localhost:8080,发现会跳转到第一个登录页面,Spring Security已经默认做了一些配置,并且创建一个简单的登录页面 ,那这个页面是怎么来的?通过跟踪源码来一探究竟。
Spring Security 官网文档链接:
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-oauth2login
跳转登录页面
通过查看文档发现,WebSecurityConfigurerAdapter 提供的默认的配置 ,这个抽象类中,提供了一个方法formLogin(),内容如下:
1protected void configure(HttpSecurity http) throws Exception {
2 logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
3
4 http
5 .authorizeRequests()
6 .anyRequest().authenticated()
7 .and()
8 .formLogin().and()
9 .httpBasic();
10 }
1public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
2 return getOrApply(new FormLoginConfigurer<>());
3 }
查看formLogin()源码,跳转到HttpSecurity类中,这个方法返回一个 FormLoginConfigurer<HttpSecurity>类型,再继续来看这个FormLoginConfigurer,在FormLoginConfigurer中有个initDefaultLoginFilter()方法
1private void initDefaultLoginFilter(H http) {
2 DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
3 .getSharedObject(DefaultLoginPageGeneratingFilter.class);
4 if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {
5 loginPageGeneratingFilter.setFormLoginEnabled(true);
6 loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter());
7 loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter());
8 loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());
9 loginPageGeneratingFilter.setFailureUrl(getFailureUrl());
10 loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());
11 }
12 }
这个方法,初始化一个默认登录页的过滤器,可以看到第一句代码,默认的过滤器是DefaultLoginPageGeneratingFilter ,进入过滤器中 :
1public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
2 throws IOException, ServletException {
3 HttpServletRequest request = (HttpServletRequest) req;
4 HttpServletResponse response = (HttpServletResponse) res;
5
6 boolean loginError = isErrorPage(request);
7 boolean logoutSuccess = isLogoutSuccess(request);
8 if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
9 String loginPageHtml = generateLoginPageHtml(request, loginError,
10 logoutSuccess);
11 .........
14
15 return;
16 }
17
18 chain.doFilter(request, response);
19 }
可以看到,如果没有配置login页,这个过滤器会被创建,然后看doFilter()方法,登录页面的配置是通过generateLoginPageHtml()方法创建的,再来看这个方法 :
1private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
2 boolean logoutSuccess) {
3 String errorMsg = "Invalid credentials";
4
5 if (loginError) {
6 HttpSession session = request.getSession(false);
7
8 if (session != null) {
9 AuthenticationException ex = (AuthenticationException) session
10 .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
11 errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";
12 }
13 }
14
15 StringBuilder sb = new StringBuilder();
16
17 sb.append("<!DOCTYPE html>\n"
18 + "<html lang=\"en\">\n"
19 + " <head>.......");
30
31 String contextPath = request.getContextPath();
32 if (this.formLoginEnabled) {
33 sb.append(" <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n"
34 + "........");
49 }
50
51 if (openIdEnabled) {
52 sb.append(" <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n"
53 .......
64 }
65
66 if (oauth2LoginEnabled) {
67 sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
68 .......
71 for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
72 .........
79 }
80 sb.append("</table>\n");
81 }
82 sb.append("</div>\n");
83 sb.append("</body></html>");
84
85 return sb.toString();
86 }
至此,默认登录页及配置,已经可以清楚了 。
用户名和密码分析
在项目启动的日志中,可以发现有这样一条信息 :
12019-05-20 16:26:24.846 INFO 9032 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :
2
3Using generated security password: 5baa53a8-2ff7-4fea-9e10-185b5f640dad
可以看到,自动配置类是UserDetailsServiceAutoConfiguration,密码是 :5baa53a8-2ff7-4fea-9e10-185b5f640dad,现在知道了密码,那用户名是什么还不知道,进入到 UserDetailsServiceAutoConfiguration去看看:
在这个 UserDetailsServiceAutoConfiguration 类的描述中可以知道,这个类是设置一些 Spring Security 相关默认的自动配置,把InMemoryUserDetailsManager 中得user 和 password 信息设置为默认得用户和密码,可以通过提供的AuthenticationManager、AuthenticationProvider 或者 UserDetailsService 的 bean 来覆盖默认的自动配置信息:
1@Bean
2 @ConditionalOnMissingBean(
3 type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
4 @Lazy
5 public InMemoryUserDetailsManager inMemoryUserDetailsManager(
6 SecurityProperties properties,
7 ObjectProvider<PasswordEncoder> passwordEncoder) {
8 SecurityProperties.User user = properties.getUser();
9 List<String> roles = user.getRoles();
10 return new InMemoryUserDetailsManager(User.withUsername(user.getName())
11 .password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
12 .roles(StringUtils.toStringArray(roles)).build());
13 }
14
15private String getOrDeducePassword(SecurityProperties.User user,
16 PasswordEncoder encoder) {
17 String password = user.getPassword();
18 if (user.isPasswordGenerated()) {
19 logger.info(String.format("%n%nUsing generated security password: %s%n",
20 user.getPassword()));
21 }
22 if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
23 return password;
24 }
25 return NOOP_PASSWORD_PREFIX + password;
26 }
可以看到, 日志输出的密码是通过inMemoryUserDetailsManager()方法获取,返回一个新的带有UserDetials信息参数构造的InMemoryUSerDetailsManager对象 ,第一个参数为:User.withUsername(user.getName()),其中user 对象是上面SecurityProperties.User类型 的,通过SecurityProperties 对象中获取的 ,看下SecurityProperties类 :
1@ConfigurationProperties(prefix = "spring.security")
2public class SecurityProperties {
3 private User user = new User();
4
5 public User getUser() {
6 return this.user;
7 }
8
9 public static class User {
10 private String name = "user";
11 private String password = UUID.randomUUID().toString();
12 }
13}
通过配置文件中的,前缀为spring.security 的配置可以改变默认配置信息,再看看SecurityProperties 的 getUser()方法 ,通过一步步的跟踪,发现默认的用户名是user 。
框架核心过滤器
想要对WEB资源进行保护,最好的办法就是Filter,想要对方法进行保护,最好的办法就是AOP,SpringSecurity在我们进行用户认证和授权的时候,会通过各种各样的拦截器来控制权限的访问,从而实现安全。SpringSecurity常见的过滤器有:
Filter | 含义 |
---|---|
WebAsyncManagerIntegrationFilter | 异步 , 提供了对securityContext和WebAsyncManager的集成 |
SecurityContextPersistenceFilter | 同步 , 从配置的SecurityContextRepository而不是request中获取信息存到SecurityContextHolder,并且当请求结束清理contextHolder时将值存回repository中(默认使用HttpSessionSecurityContextRepository).在该过滤器中每一个请求仅执行一次,该filter需在任何认证处理机制其作用之前执行。认证处理机制如basic,cas等期望在执行时从SecurityContextHolder中获取SecurityContext |
HeaderWriterFilter | 是一个向HttpServletResponse写入http请求头的约定 |
CsrfFilter | 通过使用同步token模式来进行csrf防护 |
LogoutFilter | 记录用户的退出 |
RequestCacheAwareFilter | 用于用户登录成功后,重新恢复因为登录被打断的请求 , 请求信息被保存到cache中 |
SecurityContextHolderAwareRequestFilter | 包装请求对象request |
AnonymousAuthenticationFilter | 是在UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter这些过滤器后面的,所以如果这三个过滤器都没有认证成功,则为当前的SecurityContext中添加一个经过匿名认证的token,但是通过servlet的getRemoteUser等方法是获取不到登录账号的。因为SecurityContextHolderAwareRequestFilter过滤器在AnonymousAuthenticationFilter前面 |
SessionManagementFilter | 管理session |
ExceptionTranslationFilter | 处理过滤器链抛出的所有AccessDeniedException和AuthenticationException异常 |
FilterSecurityInterceptor | 通过实现了filter来增加http资源的安全性。这个安全拦截器需要FilterInvocationSecurityMedataSource |
UsernamePasswordAuthenticationFilter | 登陆用户密码验证过滤器 ,基于用户名和密码的认证逻辑 |
BasicAuthenticationFilter | 处理一个http请求的basic认证头,将结果放入SecurityContextHolder |
DefaultLoginPageGeneratingFilter | 当一个用户没有配置login页面时使用。仅当跳转到login页面时用到 |
核心组件
Authentication
-
Authentication 是一个接口,用来表示用户认证信息的
-
在用户登录认证之前相关信息会封装为一个Authentication 具体实现类的对象 ,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用
-
Authentication 对象不需要我们自己去创建,在与系统交互的过程中,Spring Security 会自动为我们创建相应的 Authentication 对象 ,然后赋值给当前的 SecurityContext ,但是往往我们需要在程序中获取当前用户的相关信息,比如最常见的是获取当前登录用户的用户名。在程序的任何地方,通过如下方式我们可以获取到当前用户的用户名
1public String getCurrentUsername() {
2 Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
3 if (principal instanceof UserDetails) {
4 return ((UserDetails) principal).getUsername();
5 }
6 if (principal instanceof Principal) {
7 return ((Principal) principal).getName();
8 }
9 return String.valueOf(principal);
10 }
通过 Authentication.getPrincipal() 可以获取到代表当前用户的信息。
-
获取当前用户的用户名是一种比较常见的需求,关于上述代码其实 Spring Security 在 Authentication 中的实现类中已经为我们做了相关实现,所以获取当前用户的用户名最简单的方式应当如下 :
1public String getCurrentUsername() {
2 return SecurityContextHolder.getContext().getAuthentication().getName();
3 }
此外,调用 SecurityContextHolder.getContext() 获取 SecurityContext 时,如果对应的 SecurityContext 不存在,则 Spring Security 将为我们建立一个空的 SecurityContext 并进行返回
SecurityContextHolder
-
SecurityContextHolder 是用来保存 SecurityContext的
-
SecurityContext 中含有当前正在访问系统的用户的详细信息
-
默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext ,这也就意味着在处于同一线程中的方法中我们可以从 ThreadLocal 中获取到当前的 SecurityContext,因为线程池的原因,如果我们每次在请求完成后都将 ThreadLocal 进行清除的话,那么我们把 SecurityContext 存放在 ThreadLocal 中还是比较安全的
-
这些工作 Spring Security 已经自动为我们做了,即在每一次 request 结束后都将清除当前线程的 ThreadLocal
-
SecurityContextHolder 中定义了一系列的静态方法,而这些静态方法内部逻辑基本上都是通过SecurityContextHolder 持有的 SecurityContextHolderStrategy 来实现的,如 getContext()、setContext() 、clearContext()等
-
Spring Security 还提供了两种类型的 strategy 实现,GlobalSecurityContextHolderStrategy 和 InheritableThreadLocalSecurityContextHolderStrategy ,前者表示全局使用同一个 SecurityContext;后者使用 InheritableThreadLocal 来存放 SecurityContext,即子线程可以使用父线程中存放的变量
-
一般而言,我们使用默认的 strategy 就可以了,但是如果要改变默认的 strategy,Spring Security 为我们提供了两种方法,这两种方式都是通过改变 strategyName 来实现的
-
SecurityContextHolder 中为三种不同类型的 strategy 分别命名为 MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL 和 MODE_GLOBAL ,第一种方式是通过 SecurityContextHolder 的静态方法 setStrategyName() 来指定需要使用的 strategy;第二种方式是通过系统属性进行指定,其中属性名默认为 “spring.security.strategy”,属性值为对应 strategy 的名称
AuthenticationManager
AuthenticationProvider
-
AuthenticationManager 是一个用来处理认证(Authentication)请求的接口
-
认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider
-
在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回
1Authentication authenticate(Authentication authentication) throws AuthenticationException;
在 Spring Security 中,AuthenticationManager 的默认实现是 ProviderManager而且它不直接自己处理认证请求,而是委托给 其所配置的 AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证
-
如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果
-
如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException
-
校验认证请求最常用的方法是根据请求的用户名加载对应的 UserDetails,然后比对 UserDetails 的密码与认证请求的密码是否一致,一致则表示认证通过
-
Spring Security 内部的 DaoAuthenticationProvider 就是使用的这种方式。其内部使用 UserDetailsService 来负责加载 UserDetails ,在认证成功以后会使用加载的 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext 中
-
默认情况下,在认证成功后 ProviderManager 将清除返回的 Authentication 中的凭证信息,如密码
-
如果你在无状态的应用中将返回的 Authentication 信息缓存起来了,那么以后你再利用缓存的信息去认证将会失败,因为它已经不存在密码这样的凭证信息了
-
所以在使用缓存的时候你应该考虑到这个问题。一种解决办法是设置 ProviderManager 的 eraseCredentialsAfterAuthentication 属性为 false,或者想办法在缓存时将凭证信息一起缓存
UserDetailsService
-
通过 Authentication.getPrincipal() 的返回类型是 Object,但很多情况下其返回的其实是一个 UserDetails 的实例
-
UserDetails 是 Spring Security 中一个核心的接口 ,其中定义了一些可以获取用户名、密码、权限等与认证相关的信息的方法
-
Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,我们如果要使用 UserDetails 时也可以直接使用该类
-
在 Spring Security 内部很多地方需要使用用户信息的时候基本上都是使用的 UserDetails,比如在登录认证的时候
-
登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 进行认证,认证通过后会将该 UserDetails 赋给认证通过的 Authentication 的 principal,然后再把该 Authentication 存入到 SecurityContext 中
-
之后如果需要使用用户信息的时候就是通过 SecurityContextHolder 获取存放在 SecurityContext 中的 Authentication 的 principal
-
通常我们需要在应用中获取当前用户的其它信息,如 Email、电话等。这时存放在 Authentication 的 principal 中只包含有认证相关信息的 UserDetails 对象可能就不能满足我们的要求了。这时我们可以实现自己的 UserDetails,在该实现类中我们可以定义一些获取用户其它信息的方法,这样将来我们就可以直接从当前 SecurityContext 的 Authentication 的 principal 中获取这些信息了
-
UserDetailsService 也是一个接口,我们也需要实现自己的 UserDetailsService 来加载我们自定义的 UserDetails 信息。然后把它指定给 AuthenticationProvider 即可
-
另外 Spring Security 还为我们提供了 UserDetailsService 另外一个实现,InMemoryDaoImpl
-
InMemoryDaoImpl 主要是测试用的,其只是简单的将用户信息保存在内存中
GrantedAuthority
-
Authentication 的 getAuthorities() 可以返回当前 Authentication 对象拥有的权限,即当前用户拥有的权限,其返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种权限
-
GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后赋予给 UserDetails
-
GrantedAuthority 中只定义了一个 getAuthority() 方法,该方法返回一个字符串,表示对应权限的字符串表示,如果对应权限不能用字符串表示,则应当返回 null
-
Spring Security 针对 GrantedAuthority 有一个简单实现 SimpleGrantedAuthority。该类只是简单的接收一个表示权限的字符串。Spring Security 内部的所有 AuthenticationProvider 都是使用 SimpleGrantedAuthority 来封装 Authentication 对象
认证过程梳理
-
用户使用用户名和密码进行登录
-
Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken
-
将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证
-
AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象
-
通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext
-
在认证成功后,用户就可以继续操作去访问其它受保护的资源了,但是在访问的时候将会使用保存在 SecurityContext 中的 Authentication 对象进行相关的权限鉴定,如不存在对应的访问权限,则会返回 403 错误码
SpringBoot整合SpringSecurity
-
新建Maven工程,并导入以下包:
1<dependencies>
2 <dependency>
3 <groupId>org.springframework.boot</groupId>
4 <artifactId>spring-boot-starter-security</artifactId>
5 </dependency>
6 <dependency>
7 <groupId>org.springframework.boot</groupId>
8 <artifactId>spring-boot-starter-thymeleaf</artifactId>
9 </dependency>
10 <dependency>
11 <groupId>org.springframework.boot</groupId>
12 <artifactId>spring-boot-starter-web</artifactId>
13 </dependency>
14 <dependency>
15 <groupId>org.mybatis.spring.boot</groupId>
16 <artifactId>mybatis-spring-boot-starter</artifactId>
17 <version>2.0.1</version>
18 </dependency>
19
20 <dependency>
21 <groupId>mysql</groupId>
22 <artifactId>mysql-connector-java</artifactId>
23 <version>5.1.24</version>
24 </dependency>
25 <dependency>
26 <groupId>org.projectlombok</groupId>
27 <artifactId>lombok</artifactId>
28 <optional>true</optional>
29 </dependency>
30 <dependency>
31 <groupId>org.springframework.boot</groupId>
32 <artifactId>spring-boot-starter-test</artifactId>
33 <scope>test</scope>
34 </dependency>
35 <dependency>
36 <groupId>org.springframework.security</groupId>
37 <artifactId>spring-security-test</artifactId>
38 <scope>test</scope>
39 </dependency>
40 </dependencies>
准备数据库表结构
1CREATE TABLE `user` (
2 `userId` bigint(20) NOT NULL AUTO_INCREMENT,
3 `username` varchar(100) DEFAULT NULL,
4 `password` varchar(200) DEFAULT NULL,
5 `phone` varchar(20) DEFAULT NULL,
6 PRIMARY KEY (`userId`)
7) ENGINE=InnoDB DEFAULT CHARSET=utf8
新建实体对象类
1@Data
2public class User implements UserDetails {
3 private Long userId;
4 private String username;
5 private String password;
6 private String phone;
7
8 private List<GrantedAuthority> authorities;
9
10 @Override
11 public Collection<? extends GrantedAuthority> getAuthorities() {
12 return this.authorities;
13 }
14
15 @Override
16 public boolean isAccountNonExpired() {
17 return true;
18 }
19
20 @Override
21 public boolean isAccountNonLocked() {
22 return true;
23 }
24
25 @Override
26 public boolean isCredentialsNonExpired() {
27 return true;
28 }
29
30 @Override
31 public boolean isEnabled() {
32 return true;
33 }
34}
配置application.xml文件
1# 访问端口号
2server.port=8890
3
4# 数据库相关配置
5spring.datasource.driver-class-name=com.mysql.jdbc.Driver
6spring.datasource.url=jdbc:mysql://localhost:3306/securitydemo
7spring.datasource.username=root
8spring.datasource.password=123456
9
10# MyBatis的相关配置
11# 映射文件位置
12mybatis.mapper-locations=classpath:mapper/*.xml
13# 输出SQL执行语句
14mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
增加前端登录和首页面
1login.html:
10 <form th:action="@{/login}" method="post">
11 <input type="text" id="username" name="username" placeholder="手机号"/>
12 <br/>
13 <input type="password" id="password" name="password" placeholder="密码"/>
14 <br/>
15 <p th:if="${param.authError}" style="color: red">用户名或者密码错误</p>
16 <br/>
17 <button type="submit">登录</button>
18 </form>
前台页面
1index.html:
2
3<!DOCTYPE html>
4<html lang="en" xmlns:th="http://www.thymeleaf.org">
5<head>
6 <meta charset="UTF-8">
7 <title>首页面</title>
8</head>
9<body>
10
11<h3>登录成功</h3>
12<br/>
13
14<form th:action="@{/user/logout}" method="post" id="logoutForm">
15 <button type="submit" form="logoutForm">注销</button>
16</form>
17
18</body>
19</html>
增加接口页面跳转控制器
1@Controller
2public class LoginController {
3
4 @Autowired
5 private IUserService userService;
6
7 @GetMapping("/user/toLogin")
8 public String toLogin(){
9 return "login";
10 }
11
12 @PostMapping("/user/logout")
13 public String logout(){
14 return "login";
15 }
16
17}
增加权限控制等类
1@Configuration
2@EnableWebSecurity
3public class MySecurityConfig extends WebSecurityConfigurerAdapter {
4
5 @Override
6 protected void configure(HttpSecurity http) throws Exception {
7 http.authorizeRequests()
8 .antMatchers("/user/toLogin").permitAll()
9 .and()
10 .formLogin()
11 .loginProcessingUrl("/login")
12 .failureHandler(authFailHandler())
13 .and();
14 }
15
16 @Autowired
17 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
24 auth.authenticationProvider(authProvider());
25
26 }
27
28 @Bean
29 public MyAuthProvider authProvider(){
30 return new MyAuthProvider();
31 }
32
33 @Bean
34 public MyAuthFailHandler authFailHandler(){
35 return new MyAuthFailHandler();
36 }
37}
1public class MyAuthProvider implements AuthenticationProvider {
2
3 @Autowired
4 private IUserService userService;
5
6 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
7
8 @Override
9 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
10
11 String name = authentication.getName();//获取表单提交的用户名
12 String passwordForm = (String) authentication.getCredentials();//获取表单输入的密码
13
14 User user = userService.queryUserByName(name);
15 if (user==null) {
16 throw new AuthenticationCredentialsNotFoundException("authError");
17 }
18 if(bCryptPasswordEncoder.matches(passwordForm, user.getPassword())){
19 return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
20 }else{
21 throw new BadCredentialsException("authError");
22 }
25 }
26
27 @Override
28 public boolean supports(Class<?> authentication) {
29 return true;
30 }
31}
1public class MyAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {
2
3 @Override
4 public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
5
6
7 super.setDefaultFailureUrl("/user/toLogin?"+exception.getMessage());
8 super.onAuthenticationFailure(request, response, exception);
9 }
10}
增加接口和mybatis信息
1public interface IUserService {
2
3 public User queryUserByName(String username);
4
5}
1@Service
2public class UserServiceImpl implements IUserService {
3
4 @Autowired
5 UserDao userDao;
6
7 @Override
8 public User queryUserByName(String username) {
9 User user = userDao.queryUserByName(username);
10 return user;
11 }
12}
6<mapper namespace="com.mysecurity.dao.UserDao">
7
8 <select id="queryUserByName" resultType="com.mysecurity.entity.User">
9 select * from user where username=#{username}
10 </select>
11</mapper>
1public interface UserDao {
2
3 public User queryUserByName(String username);
4}
启动运行、访问即可