目录

大纲

一、自定义资源权限规则

二、自定义登录界面

三、自定义登录成功处理

四、显示登录失败信息

五、自定义登录失败处理

六、注销登录

七、登录用户数据获取

1. SecurityContextHolder

2. SecurityContextHolderStrategy

3. 代码中获取认证之后用户数据

4. 多线程情况下获取用户数据

5. 页面上获取用户信息

八、自定义认证数据源

1. 认证流程分析

2. 三者关系

3. 配置全局 AuthenticationManager

4. 自定义内存数据源

5. 自定义数据库数据源

九、添加认证验证码

1. 配置验证码

2. 传统 web 开发

3. 前后端分离开发


大纲

  • 认证配置
  • 表单认证
  • 注销登录
  • 前后端分离认证
  • 添加验证码

一、自定义资源权限规则

  • /index  公共资源
  • /hello .... 受保护资源 权限管理

在项目中添加如下配置就可以实现对资源权限规则设定:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/index").permitAll().anyRequest().authenticated().and().formLogin();}
}

# 说明
- permitAll() 代表放行该资源,该资源为公共资源 无需认证和授权可以直接访问
- anyRequest().authenticated() 代表所有请求,必须认证之后才能访问
- formLogin() 代表开启表单认证
## 注意: 放行资源必须放在所有认证请求之前!

二、自定义登录界面

  • 引入模板依赖
<!--thymeleaf-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

  • 定义登录页面 controller
@Controller
public class LoginController {@RequestMapping("/login.html")public String login() {return "login";}
}

  • 在 templates 中定义登录界面
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录</title>
</head>
<body>
<h1>用户登录</h1>
<form method="post" th:action="@{/doLogin}">用户名:<input name="uname" type="text"/><br>密码:<input name="passwd" type="password"/><br><input type="submit" value="登录"/>
</form>
</body>
</html>


需要注意的是

    • 登录表单 method 必须为 post,action 的请求路径为 /doLogin
    • 用户名的 name 属性为 uname
    • 密码的 name 属性为 passwd
  • 配置 Spring Security 配置类
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/login.html").permitAll().mvcMatchers("/index").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login.html").loginProcessingUrl("/doLogin").usernameParameter("uname").passwordParameter("passwd").successForwardUrl("/index") 		 //forward 跳转           注意:不会跳转到之前请求路径//.defaultSuccessUrl("/index")   //redirect 重定向    注意:如果之前请求路径,会有优先跳转之前请求路径.failureUrl("/login.html").and().csrf().disable();//这里先关闭 CSRF}
}
  • successForwardUrl 、defaultSuccessUrl 这两个方法都可以实现成功之后跳转
    • successForwardUrl  默认使用 forward 跳转
      注意:不会跳转到之前请求路径
    • defaultSuccessUrl   默认使用 redirect 跳转
      注意:如果之前请求路径,会有优先跳转之前请求路径,可以传入第二个参数进行修改`

三、自定义登录成功处理

有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面。

只需要给前端返回一个 JSON 通知登录成功还是失败与否。

这个时候可以通过自定义 AuthenticationSucccessHandler 实现

public interface AuthenticationSuccessHandler {/*** Called when a user has been successfully authenticated.* @param request the request which caused the successful authentication* @param response the response* @param authentication the <tt>Authentication</tt> object which was created during* the authentication process.*/void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException;
}

根据接口的描述信息,也可以得知登录成功会自动回调这个方法,进一步查看它的默认实现,你会发现

successForwardUrl、defaultSuccessUrl也是由它的子类实现的

  • 自定义 AuthenticationSuccessHandler 实现
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登录成功");result.put("status", 200);response.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}
  • 配置 AuthenticationSuccessHandler
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//.....successHandler(new MyAuthenticationSuccessHandler()).failureUrl("/login.html").and().csrf().disable();//这里先关闭 CSRF}
}

四、显示登录失败信息

为了能更直观在登录页面看到异常错误信息,可以在登录页面中直接获取异常信息。

Spring Security 在登录失败之后会将异常信息存储到 request 、session作用域中 key 为

SPRING_SECURITY_LAST_EXCEPTION 命名属性中,源码可以参考 SimpleUrlAuthenticationFailureHandler :

  • 显示异常信息
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录</title>
</head>
<body>....<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
</body>
</html>
  • 配置
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//...and().formLogin()//....//.failureUrl("/login.html").failureForwardUrl("/login.html").and().csrf().disable();//这里先关闭 CSRF}
}
  • failureUrl、failureForwardUrl 关系类似于之前提到的 successForwardUrl 、defaultSuccessUrl 方法
    • failureUrl 失败以后的重定向跳转
    • failureForwardUrl 失败以后的 forward 跳转
      注意:因此获取 request 中异常信息,这里只能使用failureForwardUrl

五、自定义登录失败处理

和自定义登录成功处理一样,Spring Security 同样为前后端分离开发提供了登录失败的处理,这个类就是

AuthenticationFailureHandler,源码为:

public interface AuthenticationFailureHandler {/*** Called when an authentication attempt fails.* @param request the request during which the authentication attempt occurred.* @param response the response.* @param exception the exception which was thrown to reject the authentication* request.*/void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException;}

根据接口的描述信息,也可以得知登录失败会自动回调这个方法,进一步查看它的默认实现,你会发现

failureUrl、failureForwardUrl也是由它的子类实现的。

  • 自定义 AuthenticationFailureHandler 实现
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登录失败: "+exception.getMessage());result.put("status", 500);response.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}
  • 配置 AuthenticationFailureHandler
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//...failureHandler(new MyAuthenticationFailureHandler()).and().csrf().disable();//这里先关闭 CSRF}
}

六、注销登录

Spring Security 中也提供了默认的注销登录配置,在开发时也可以按照自己需求对注销进行个性化定制。

  • 开启注销登录默认开启
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//....and().logout().logoutUrl("/logout").invalidateHttpSession(true).clearAuthentication(true).logoutSuccessUrl("/login.html").and().csrf().disable();//这里先关闭 CSRF}
}
    • 通过 logout() 方法开启注销配置
    • logoutUrl 指定退出登录请求地址,默认是 GET 请求,路径为 /logout
    • invalidateHttpSession 退出时是否是 session 失效,默认值为 true
    • clearAuthentication 退出时是否清除认证信息,默认值为 true
    • logoutSuccessUrl 退出登录时跳转地址
  • 配置多个注销登录请求
    如果项目中有需要,开发者还可以配置多个注销登录的请求,同时还可以指定请求的方法:
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//....and().logout().logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),new AntPathRequestMatcher("/logout","GET"))).invalidateHttpSession(true).clearAuthentication(true).logoutSuccessUrl("/login.html").and().csrf().disable();//这里先关闭 CSRF}
}
  • 前后端分离注销登录配置
    如果是前后端分离开发,注销成功之后就不需要页面跳转了,只需要将注销成功的信息返回前端即可,此时我们可以通过自定义 LogoutSuccessHandler  实现来返回注销之后信息:
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "注销成功");result.put("status", 200);response.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//.....and().formLogin()//....and().logout()//.logoutUrl("/logout").logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),new AntPathRequestMatcher("/logout","GET"))).invalidateHttpSession(true).clearAuthentication(true)//.logoutSuccessUrl("/login.html").logoutSuccessHandler(new MyLogoutSuccessHandler()).and().csrf().disable();//这里先关闭 CSRF}
}

七、登录用户数据获取

1. SecurityContextHolder

Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,Spring Security在此基

础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录

成功的用户信息保存到 SecurityContextHolder 中。

SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的

变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录

请求处理完毕后,Spring Security 会将 SecurityContextHolder中的数据拿出来保存到 Session 中,同时

将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从Session 中

取出用户登录数据,保存到SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请

求结束时将SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder

中的数据清空。

实际上 SecurityContextHolder 中存储是 SecurityContext,在 SecurityContext 中存储是

Authentication。

这种设计是典型的策略设计模式:

public class SecurityContextHolder {public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";public static final String MODE_GLOBAL = "MODE_GLOBAL";private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";private static SecurityContextHolderStrategy strategy;//....private static void initializeStrategy() {if (MODE_PRE_INITIALIZED.equals(strategyName)) {Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED+ ", setContextHolderStrategy must be called with the fully constructed strategy");return;}if (!StringUtils.hasText(strategyName)) {// Set defaultstrategyName = MODE_THREADLOCAL;}if (strategyName.equals(MODE_THREADLOCAL)) {strategy = new ThreadLocalSecurityContextHolderStrategy();return;}if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {strategy = new InheritableThreadLocalSecurityContextHolderStrategy();return;}if (strategyName.equals(MODE_GLOBAL)) {strategy = new GlobalSecurityContextHolderStrategy();return;}//.....}
}
  1. MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal中,大家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达 Servlet,都是由一个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。
  2. MODE INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
  3. MODE GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在 JavaWeb开发中,这种模式很少使用到。

2. SecurityContextHolderStrategy

通过 SecurityContextHolder 可以得知,SecurityContextHolderStrategy 接口用来定义存储策略方法

public interface SecurityContextHolderStrategy {void clearContext();SecurityContext getContext();void setContext(SecurityContext context);SecurityContext createEmptyContext();
}

接口中一共定义了四个方法:

  • clearContext:该方法用来清除存储的 SecurityContext对象。
  • getContext:该方法用来获取存储的 SecurityContext 对象。
  • setContext:该方法用来设置存储的 SecurityContext 对象。
  • create Empty Context:该方法则用来创建一个空的 SecurityContext 对象。

从上面可以看出每一个实现类对应一种策略的实现。

3. 代码中获取认证之后用户数据

@RestController
public class HelloController {@RequestMapping("/hello")public String hello() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();User principal = (User) authentication.getPrincipal();System.out.println("身份 :"+principal.getUsername());System.out.println("凭证 :"+authentication.getCredentials());System.out.println("权限 :"+authentication.getAuthorities());return "hello security";}
}

4. 多线程情况下获取用户数据

@RestController
public class HelloController {@RequestMapping("/hello")public String hello() {new Thread(()->{Authentication authentication = SecurityContextHolder.getContext().getAuthentication();User principal = (User) authentication.getPrincipal();System.out.println("身份 :"+principal.getUsername());System.out.println("凭证 :"+authentication.getCredentials());System.out.println("权限 :"+authentication.getAuthorities());}).start();return "hello security";}
}

可以看到默认策略,是无法在子线程中获取用户信息,如果需要在子线程中获取必须使用第二种策略,默认策略

是通过 System.getProperty 加载的,因此我们可以通过增加 VM Options 参数进行修改。

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

5. 页面上获取用户信息

  • 引入依赖
<dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId><version>3.0.4.RELEASE</version>
</dependency>
  • 页面加入命名空间
<html lang="en" xmlns:th="https://www.thymeleaf.org" 
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
  • 页面中使用
<!--获取认证用户名-->
<ul><li sec:authentication="principal.username"></li><li sec:authentication="principal.authorities"></li><li sec:authentication="principal.accountNonExpired"></li><li sec:authentication="principal.accountNonLocked"></li><li sec:authentication="principal.credentialsNonExpired"></li>
</ul>

八、自定义认证数据源

1. 认证流程分析

Servlet Authentication Architecture :: Spring Security

  • 发起认证请求,请求中携带用户名、密码,该请求会被 UsernamePasswordAuthenticationFilter  拦截
  • 在UsernamePasswordAuthenticationFilter的attemptAuthentication方法中将请求中用户名和密码,封装为Authentication 对象,并交给AuthenticationManager 进行认证
  • 认证成功,将认证信息存储到 SecurityContextHodler 以及调用记住我等,并回调AuthenticationSuccessHandler 处理
  • 认证失败,清除 SecurityContextHodler 以及 记住我中信息,回调 AuthenticationFailureHandler 处理

2. 三者关系

从上面分析中得知,AuthenticationManager 是认证的核心类,但实际上在底层真正认证时还离不开

ProviderManager 以及AuthenticationProvider 。他们三者关系是样的呢?

  • AuthenticationManager 是一个认证管理器,它定义了 Spring Security 过滤器要执行认证操作。
  • ProviderManager是AuthenticationManager接口的实现类。Spring Security 认证时默认使用就是 ProviderManager。
  • AuthenticationProvider 就是针对不同的身份类型执行的具体的身份认证。

AuthenticationManager 与 ProviderManager

ProviderManager 是 AuthenticationManager 的唯一实现,也是 Spring Security 默认使用实现。

从这里不难看出默认情况下

AuthenticationManager 就是一个ProviderManager。

ProviderManager 与 AuthenticationProvider

摘自官方: Servlet Authentication Architecture :: Spring Security

在 Spring Seourity 中,允许系统同时支持多种不同的认证方式,例如同时支持用户名/密码认证、

ReremberMe 认证、手机号码动态认证等,而不同的认证方式对应了不同的 AuthenticationProvider,所

以一个完整的认证流程可能由多个 AuthenticationProvider 来提供。

多个 AuthenticationProvider 将组成一个列表,这个列表将由 ProviderManager 代理。换句话说,

在ProviderManager 中存在一个AuthenticationProvider 列表,在Provider Manager 中遍历列表中的每

一个 AuthenticationProvider 去执行身份认证,最终得到认证结果。

ProviderManager 本身也可以再配置一个 AuthenticationManager 作为 parent,这样当

ProviderManager 认证失败之后,就可以进入到 parent 中再次进行认证。

理论上来说,ProviderManager 的 parent 可以是任意类型的 AuthenticationManager,但是通常都

是由ProviderManager 来扮演 parent 的角色,也就是 ProviderManager 是 ProviderManager 的

parent。

ProviderManager 本身也可以有多个,多个ProviderManager 共用同一个 parent。有时,一个应用

程序有受保护资源的逻辑组(例如,所有符合路径模式的网络资源,如/api/**),每个组可以有自己的专

用 AuthenticationManager。

通常,每个组都是一个ProviderManager,它们共享一个父级。然后,父级是一种 全局资源,作为

所有提供者的后备资源。

根据上面的介绍,我们绘出新的 AuthenticationManager、ProvideManager 和

AuthentictionProvider 关系

摘自官网: Architecture :: Spring Security

弄清楚认证原理之后我们来看下具体认证时数据源的获取。

默认情况下 AuthenticationProvider 是由 DaoAuthenticationProvider 类来实现认证的,

DaoAuthenticationProvider 认证时又通过 UserDetailsService 完成数据源的校验。他们之

间调用关系如下:

总结:AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也

可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的

AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是

由 ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列

表中每一个实现代表一种身份认证方式。认证时底层数据源需要调用 UserDetailService 来实现。

3. 配置全局 AuthenticationManager

Architecture :: Spring Security

  • 默认的全局 AuthenticationManager
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowiredpublic void initialize(AuthenticationManagerBuilder builder) {//builder..}
}
    • springboot 对 security 进行自动配置时自动在工厂中创建一个全局AuthenticationManager

总结:

    1. 默认自动配置创建全局AuthenticationManager 默认找当前项目中是否存在自定义 UserDetailService 实例 自动将当前项目UserDetailService 实例设置为数据源
    2. 默认自动配置创建全局AuthenticationManager 在工厂中使用时直接在代码中注入即可
  • 自定义全局 AuthenticationManager
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overridepublic void configure(AuthenticationManagerBuilder builder) {//builder ....}
}
    • 自定义全局 AuthenticationManager

总结

    1. 一旦通过 configure 方法自定义 AuthenticationManager实现 就回将工厂中自动配置AuthenticationManager 进行覆盖
    2. 一旦通过 configure 方法自定义 AuthenticationManager实现 需要在实现中指定认证数据源对象 UserDetaiService 实例
    3. 一旦通过 configure 方法自定义 AuthenticationManager实现 这种方式创建AuthenticationManager对象工厂内部本地一个AuthenticationManager 对象 不允许在其他自定义组件中进行注入
  • 用来在工厂中暴露自定义AuthenticationManager 实例
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {//1.自定义AuthenticationManager  推荐  并没有在工厂中暴露出来@Overridepublic void configure(AuthenticationManagerBuilder builder) throws Exception {System.out.println("自定义AuthenticationManager: " + builder);builder.userDetailsService(userDetailsService());}//作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}

4. 自定义内存数据源

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager= new InMemoryUserDetailsManager();UserDetails u1 = User.withUsername("zhangs").password("{noop}111").roles("USER").build();inMemoryUserDetailsManager.createUser(u1);return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}  	
}

5. 自定义数据库数据源

  • 设计表结构
-- 用户表
CREATE TABLE `user`
(`id`                    int(11) NOT NULL AUTO_INCREMENT,`username`              varchar(32)  DEFAULT NULL,`password`              varchar(255) DEFAULT NULL,`enabled`               tinyint(1) DEFAULT NULL,`accountNonExpired`     tinyint(1) DEFAULT NULL,`accountNonLocked`      tinyint(1) DEFAULT NULL,`credentialsNonExpired` tinyint(1) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 角色表
CREATE TABLE `role`
(`id`      int(11) NOT NULL AUTO_INCREMENT,`name`    varchar(32) DEFAULT NULL,`name_zh` varchar(32) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 用户角色关系表
CREATE TABLE `user_role`
(`id`  int(11) NOT NULL AUTO_INCREMENT,`uid` int(11) DEFAULT NULL,`rid` int(11) DEFAULT NULL,PRIMARY KEY (`id`),KEY   `uid` (`uid`),KEY   `rid` (`rid`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
  • 插入测试数据
-- 插入用户数据
BEGIN;INSERT INTO `user`VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);INSERT INTO `user`VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);INSERT INTO `user`VALUES (3, 'blr', '{noop}123', 1, 1, 1, 1);
COMMIT;
-- 插入角色数据
BEGIN;INSERT INTO `role`VALUES (1, 'ROLE_product', '商品管理员');INSERT INTO `role`VALUES (2, 'ROLE_admin', '系统管理员');INSERT INTO `role`VALUES (3, 'ROLE_user', '用户管理员');
COMMIT;
-- 插入用户角色数据
BEGIN;INSERT INTO `user_role`VALUES (1, 1, 1);INSERT INTO `user_role`VALUES (2, 1, 2);INSERT INTO `user_role`VALUES (3, 2, 2);INSERT INTO `user_role`VALUES (4, 3, 3);
COMMIT;
  • 项目中引入依赖
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.38</version>
</dependency>
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.7</version>
</dependency>
  • 配置 springboot 配置文件
# datasource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root# mybatis
mybatis.mapper-locations=classpath:com/baizhi/mapper/*.xml
mybatis.type-aliases-package=com.baizhi.entity# log
logging.level.com.baizhi=debug

  • 创建 entity
    • 创建 user 对象
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<>();@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> grantedAuthorities = new ArrayList<>();roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));return grantedAuthorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}@Overridepublic 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);
}
  • 创建 UserMapper 实现
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.baizhi.dao.UserDao"><!--查询单个--><select id="loadUserByUsername" resultType="User">select id,username,password,enabled,accountNonExpired,accountNonLocked,credentialsNonExpiredfrom userwhere username = #{username}</select><!--查询指定行数据--><select id="getRolesByUid" resultType="Role">select r.id,r.name,r.name_zh nameZhfrom role r,user_role urwhere r.id = ur.ridand ur.uid = #{uid}</select>
</mapper>
  • 创建 UserDetailService 实例
@Component
public class MyUserDetailService implements UserDetailsService {private  final UserDao userDao;@Autowiredpublic MyUserDetailService(UserDao userDao) {this.userDao = userDao;}@Overridepublic 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;@Autowiredpublic WebSecurityConfigurer(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}@Overrideprotected void configure(AuthenticationManagerBuilder builder) throws Exception {builder.userDetailsService(userDetailsService);}@Overrideprotected void configure(HttpSecurity http) throws Exception {//web security..}
}
  • 启动测试即可

九、添加认证验证码

1. 配置验证码

<dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version>
</dependency>
@Configuration
public class KaptchaConfig {@Beanpublic Producer kaptcha() {Properties properties = new Properties();properties.setProperty("kaptcha.image.width", "150");properties.setProperty("kaptcha.image.height", "50");properties.setProperty("kaptcha.textproducer.char.string", "0123456789");properties.setProperty("kaptcha.textproducer.char.length", "4");Config config = new Config(properties);DefaultKaptcha defaultKaptcha = new DefaultKaptcha();defaultKaptcha.setConfig(config);return defaultKaptcha;}
}

2. 传统 web 开发

  • 生成验证码 controller
@Controller
public class KaptchaController {private final Producer producer;@Autowiredpublic KaptchaController(Producer producer) {this.producer = producer;}@GetMapping("/vc.jpg")public void getVerifyCode(HttpServletResponse response, HttpSession session) throws IOException {response.setContentType("image/png");String code = producer.createText();session.setAttribute("kaptcha", code);//可以更换成 redis 实现BufferedImage bi = producer.createImage(code);ServletOutputStream os = response.getOutputStream();ImageIO.write(bi, "jpg", os);}
}
  • 自定义验证码异常类
public class KaptchaNotMatchException extends AuthenticationException {public KaptchaNotMatchException(String msg) {super(msg);}public KaptchaNotMatchException(String msg, Throwable cause) {super(msg, cause);}
}
  • 自定义filter验证验证码
public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {public static final String KAPTCHA_KEY = "kaptcha";//默认值private String kaptcha = KAPTCHA_KEY;public String getKaptcha() {return kaptcha;}public void setKaptcha(String kaptcha) {this.kaptcha = kaptcha;}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {//1.判断是否是 post 方式if (request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}//2.获取验证码String kaptcha = request.getParameter(getKaptcha());String sessionKaptcha = (String) request.getSession().getAttribute("kaptcha");if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionKaptcha) &&kaptcha.equalsIgnoreCase(sessionKaptcha)) {return super.attemptAuthentication(request, response);}throw new KaptchaNotMatchException("验证码输入错误!");}
}
  • 放行以及配置验证码 filter
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {private final UserDetailsService userDetailsService;@Autowiredpublic WebSecurityConfigurer(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}@Overrideprotected void configure(AuthenticationManagerBuilder builder) throws Exception {builder.userDetailsService(userDetailsService);}@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Beanpublic KaptchaFilter kaptchaFilter() throws Exception {KaptchaFilter kaptchaFilter = new KaptchaFilter();//指定接收验证码请求参数名kaptchaFilter.setKaptcha("kaptcha");//指定认证管理器kaptchaFilter.setAuthenticationManager(authenticationManagerBean());//指定认证成功和失败处理kaptchaFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());kaptchaFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());//指定处理登录kaptchaFilter.setFilterProcessesUrl("/doLogin");kaptchaFilter.setUsernameParameter("uname");kaptchaFilter.setPasswordParameter("passwd");return kaptchaFilter;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/vc.jpg").permitAll().mvcMatchers("/login.html").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login.html")...http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);}
}
  • 登录页面添加验证码
<form method="post" th:action="@{/doLogin}">用户名:<input name="uname" type="text"/><br>密码:<input name="passwd" type="password"/><br>验证码: <input name="kaptcha" type="text"/> <img alt="" th:src="@{/vc.jpg}"><br><input type="submit" value="登录"/>
</form>

3. 前后端分离开发

  • 生成验证码 controller
@RestController
public class KaptchaController {private final Producer producer;@Autowiredpublic KaptchaController(Producer producer) {this.producer = producer;}@GetMapping("/vc.png")public String getVerifyCode(HttpSession session) throws IOException {//1.生成验证码String code = producer.createText();session.setAttribute("kaptcha", code);//可以更换成 redis 实现BufferedImage bi = producer.createImage(code);//2.写入内存FastByteArrayOutputStream fos = new FastByteArrayOutputStream();ImageIO.write(bi, "png", fos);//3.生成 base64return Base64.encodeBase64String(fos.toByteArray());}
}
  • 定义验证码异常类
public class KaptchaNotMatchException extends AuthenticationException {public KaptchaNotMatchException(String msg) {super(msg);}public KaptchaNotMatchException(String msg, Throwable cause) {super(msg, cause);}
}
  • 在自定义LoginKaptchaFilter中加入验证码验证
//自定义 filter
public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {public static final String FORM_KAPTCHA_KEY = "kaptcha";private String kaptchaParameter = FORM_KAPTCHA_KEY;public String getKaptchaParameter() {return kaptchaParameter;}public void setKaptchaParameter(String kaptchaParameter) {this.kaptchaParameter = kaptchaParameter;}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (!request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}try {//1.获取请求数据Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);String kaptcha = userInfo.get(getKaptchaParameter());//用来获取数据中验证码String username = userInfo.get(getUsernameParameter());//用来接收用户名String password = userInfo.get(getPasswordParameter());//用来接收密码//2.获取 session 中验证码String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionVerifyCode) &&kaptcha.equalsIgnoreCase(sessionVerifyCode)) {//3.获取用户名 和密码认证UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}} catch (IOException e) {e.printStackTrace();}throw new KaptchaNotMatchException("验证码不匹配!");}
}
  • 配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {//自定义内存数据源@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//配置@Beanpublic LoginKaptchaFilter loginKaptchaFilter() throws Exception {LoginKaptchaFilter loginKaptchaFilter = new LoginKaptchaFilter();//1.认证 urlloginKaptchaFilter.setFilterProcessesUrl("/doLogin");//2.认证 接收参数loginKaptchaFilter.setUsernameParameter("uname");loginKaptchaFilter.setPasswordParameter("passwd");loginKaptchaFilter.setKaptchaParameter("kaptcha");//3.指定认证管理器loginKaptchaFilter.setAuthenticationManager(authenticationManagerBean());//4.指定成功时处理loginKaptchaFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登录成功");result.put("用户信息", authentication.getPrincipal());resp.setContentType("application/json;charset=UTF-8");resp.setStatus(HttpStatus.OK.value());String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);});//5.认证失败处理loginKaptchaFilter.setAuthenticationFailureHandler((req, resp, ex) -> {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登录失败: " + ex.getMessage());resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());resp.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);});return loginKaptchaFilter;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().mvcMatchers("/vc.jpg").permitAll().anyRequest().authenticated().and().formLogin().and().exceptionHandling().authenticationEntryPoint((req, resp, ex) -> {resp.setContentType("application/json;charset=UTF-8");resp.setStatus(HttpStatus.UNAUTHORIZED.value());resp.getWriter().println("必须认证之后才能访问!");}).and().logout().and().csrf().disable();http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);}
  • 测试验证

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/86989.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/86989.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/86989.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IPLOOK 2025上半年足迹回顾:连接全球,步履不停

2025年上半年&#xff0c;IPLOOK积极活跃于全球通信舞台&#xff0c;足迹横跨亚洲、欧洲、非洲与北美洲&#xff0c;我们围绕5G核心网、私有网络、云化架构等方向&#xff0c;向来自不同地区的客户与合作伙伴展示了领先的端到端解决方案&#xff0c;深入了解各地市场需求与技术…

【Kafka】docker 中配置带 Kerberos 认证的 Kafka 环境(全过程)

1. 准备 docker 下载镜像 docker pull centos/systemd&#xff0c;该镜像是基于 centos7 增加了 systemd 的功能&#xff0c;可以更方便启动后台服务 启动镜像 使用 systemd 功能需要权限&#xff0c;如果是模拟 gitlab services 就不要使用 systemd 的方式启动 如果不使用 s…

用Python构建一个可扩展的多网盘聚合管理工具 (以阿里云盘为例)

摘要 本文旨在从开发者视角&#xff0c;探讨并实践如何构建一个命令行界面的、支持多网盘聚合管理的工具。我们将以阿里云盘为例&#xff0c;深入解析其API认证与核心操作&#xff0c;并用Python从零开始实现文件列表、重命名、分享等功能。更重要的是&#xff0c;本文将重点讨…

筑牢网络安全屏障

在数字化浪潮席卷全球的今天&#xff0c;网络空间已成为继陆、海、空、天之后的 “第五疆域”&#xff0c;深刻影响着国家政治、经济、军事等各个领域。“没有网络安全就没有国家安全”&#xff0c;这句论断精准道出了网络安全与国家安全之间密不可分的关系。​ 网络安全关乎国…

计算机网络(一)层

一、分层 分层的意义&#xff1a;简化复杂性、提高灵活性、促进标准化 &#xff08;1&#xff09;法律上国际标准——OSI体系结构 &#xff08;2&#xff09;事实上的网络标准——TCP/IP体系结构 TCP&#xff1a;运输层的协议 IP&#xff1a;网际层的一个协议 网络接口层&…

STM32 rs485实现中断DMA模式收发不定长数据

在STM32F103上使用TD301D485H模块通过USB转485/422串口线与电脑通信 TXD (TD301D485H) -> PA2 (STM32F103)RXD (TD301D485H) -> PA3 (STM32F103)CON (TD301D485H) -> PA1 (STM32F103) 由于485是半双工通信&#xff0c;需要在发送和接收时控制方向引脚&#xff08;CO…

DDL-8-小结

DDL 小结 DDL 小结 DDL 小结DDL - 数据库操作DDL - 表操作 DDL - 数据库操作 查看当前有哪些数据库 SHOW DATABASES;新建数据库 CREATE DATABASE 数据库名;使用数据库 USE 数据库名;查询当前数据库 SELECT DATABASE();删除数据库 DROP DATABASE 数据库名;DDL - 表操作 查看当前…

Redis 安装使用教程

一、Redis 简介 Redis 是一个开源&#xff08;BSD 许可&#xff09;、内存数据结构存储系统&#xff0c;可以用作数据库、缓存和消息中间件。支持字符串、哈希、列表、集合、有序集合等数据类型&#xff0c;广泛应用于分布式缓存、排行榜、实时数据分析等场景。 二、下载安装…

Go语言测试与调试:单元测试与基准测试

以下是《Go语言实战指南》中关于 测试与调试&#xff1a;单元测试与基准测试 的详细内容&#xff0c;涵盖测试编写、运行、覆盖率分析与性能测试&#xff0c;适用于实际项目开发与性能优化阶段。 一、Go 的测试体系概览 Go 提供原生的测试工具包 testing&#xff0c;无需第三方…

数字FIR-I型滤波器设计(窗函数法)

目录 一、实验目的 二、实验原理 2.1 概念辨析 2.2 代码实现逻辑与工具函数 三、实验内容 3.1 设计带通滤波器&#xff08;电平组合法&#xff0c;&#xff08;理想宽带低通-理想窄带低通&#xff09;x窗函数&#xff09; 3.2 高通滤波器&#xff08;…

RHCSA认证题目练习一(配置网络设置)

一. 题目 配置网络设置 解题过程&#xff1a; 注意&#xff1a;不可以在xshell中完成&#xff0c;否则会直接断联 这里用图形化解题&#xff0c;更加简单防止命令记错 1. 打开图形化视图 命令&#xff1a;nmtui 按回车确认 按回车确认 2.首先把IPv4配置 <自动> 改成 …

STL简介+string模拟实现

STL简介string模拟实现 1. 什么是STL2. STL的版本3. STL的六大组件4.STL的缺陷5. string5.1 C语言中的字符串5.2 1个OJ题 6.标准库中的string类6.1 string类(了解)6.2 string类的常用接口说明1.string类对象的常见构造函数2.析构函数(~string())3.赋值函数 (operator) 6.3 stri…

golang实现一个mysql中随机获取cookies的API

之前用FASTAPI写了一个随机cookies请求的接口,现在尝试用golang实现同样的效果 1.编写go代码 package mainimport ("database/sql""encoding/json""fmt"_ "github.com/go-sql-driver/mysql""log""net/http"&quo…

[Vue2组件]三角形角标

[Vue2组件]三角形角标 <template><div class"ys-subscript" :style"svgStyle"><svg width"200" height"200" viewBox"0 0 200 200" xmlns"http://www.w3.org/2000/svg"><!-- 三角形背景 - 右…

洛谷刷题4

B4354 [GESP202506 一级] 假期阅读 题目传送门 B4354 难度&#xff1a;入门 很简单的题&#xff0c;如果小A看的页数≤这本书的页数&#xff0c;输出他看的页数 否则&#xff0c;输出这本书的页数 AC代码&#xff1a; #include <iostream> using namespace std; in…

【基于Echarts的地图可视化】

<!DOCTYPE html> <html> <head><meta charset"utf-8"><title>中国牛只分布可视化</title><script src"https://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js"></script><script src"h…

系统架构设计师备考之架构设计基础

1.计算机系统基础知识 1.1.计算机系统概述 计算机系统的定义与组成 计算机系统是指用于数据管理的计算机硬件、软件及网络组成的系统。 计算机系统可划分为硬件和软件两部分。硬件由机械、电子元器件、磁介质和光介质等物理实体构成&#xff1b; 软件是一系列按照特定顺序组织…

针对华为云服务器使用率过大

从这两张监控图可以看出&#xff0c;服务器在大约上午 10:30 前后经历了一次明显的负载变化&#xff1a; 1. 图表解读 CPU 使用率 从凌晨到上午约 10:00 前&#xff0c;CPU 基本处于 0–2% 的闲置状态。10:00–14:00 之间&#xff0c;CPU 利用率逐步攀升&#xff0c;多次冲击 3…

记dwz(JUI)前端框架使用之--服务端响应提示框

目录 前言 一、DWZ服务器端响应种类 二、如何增加info级别的消息提示 1.打开项目的BaseController.java类 2.打开项目的dwz.min.js文件 3.最后在前端DWZ的主加载页面或者js文件中添加如下代码&#xff1a; 前言 本篇文章没有讲太多东西&#xff0c;主要是个人工作记录保…

leetcode 295. 数据流的中位数

时间复杂度分析&#xff1a;为什么你的中位数查找方案会超时&#xff1f; 分析你提供的MedianFinder实现&#xff0c;其时间复杂度较高的原因主要在于findMedian函数的实现方式。让我详细解释&#xff1a; 代码时间复杂度分析 你的代码中两个主要函数的时间复杂度如下&#…