环境准备

需求分析

image.png
login.html 页面:不需要登录就可以访问
index.html 页面:登录后才能访问,在该页面中有 业务1,业务2、日志管理 和 用户管理 共4个连接
syslog.html 页面、sysuser.html 页面: 登录后,需要 admin用户才能访问
bizi1.html 页面、biz2.html 页面:登录后,就可以访问

创建父工程

父工程的工程名:spring-security-demo,在父工程中 声明使用 SpringBoot的版本号

  1. <dependencyManagement>
  2. <dependencies>
  3. <dependency>
  4. <groupId>org.springframework.boot</groupId>
  5. <artifactId>spring-boot-starter-parent</artifactId>
  6. <version>2.2.4.RELEASE</version>
  7. <type>pom</type>
  8. <scope>import</scope>
  9. </dependency>
  10. </dependencies>
  11. </dependencyManagement>

创建子工程

子工程的工程名:basic-server,主要引入 web 和 thymeleaf 依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  8. </dependency>

创建 application.yml

在该文件中定义端口号

  1. server:
  2. port: 9999

创建页面

页面存放的位置以及名称如图:
image.png

html 文件放在 static 和 templates 文件夹下的区别? html 文件放在 static 是不需要经过模板引擎的渲染,即可直接访问到 而放在 templates 下的html是经过模板引擎渲染的,也就是需要 controller 类中的方法返回字符串(视图名)

login页面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录页面</title>
  6. </head>
  7. <body>
  8. <form action="/login" method="post">
  9. <span>用户名称</span><input type="text" name="username" /> <br>
  10. <span>用户密码</span><input type="password" name="password" /> <br>
  11. <input type="submit" value="登陆">
  12. </form>
  13. </body>
  14. </html>

index页面

  1. <!DOCTYPE html>
  2. <html>
  3. <head lang="en">
  4. <meta charset="UTF-8" />
  5. </head>
  6. <body>
  7. <br>
  8. <a href="/syslog">日志管理</a>
  9. <br>
  10. <a href="/sysuser">用户管理</a>
  11. <br>
  12. <a href="/biz1">具体业务一</a>
  13. <br>
  14. <a href="/biz2">具体业务二</a>
  15. </body>
  16. </html>

业务1页面

  1. <h1>具体业务一</h1>

业务2页面

  1. <h1>具体业务二</h1>

日志管理页面

  1. <h1>日志管理</h1>

用户管理页面

  1. <h1>用户管理</h1>

main方法

  1. @SpringBootApplication
  2. public class Application {
  3. public static void main(String[] args) {
  4. SpringApplication.run(Application.class, args);
  5. }
  6. }

controller处理请求

  1. @Controller
  2. public class HelloController {
  3. // 首页
  4. @GetMapping("/index")
  5. public String index() {
  6. return "index";
  7. }
  8. // 日志管理
  9. @GetMapping("/syslog")
  10. public String showOrder() {
  11. return "syslog";
  12. }
  13. // 用户管理
  14. @GetMapping("/sysuser")
  15. public String addOrder() {
  16. return "sysuser";
  17. }
  18. // 具体业务一
  19. @GetMapping("/biz1")
  20. public String updateOrder() {
  21. return "biz1";
  22. }
  23. // 具体业务二
  24. @GetMapping("/biz2")
  25. public String deleteOrder() {
  26. return "biz2";
  27. }
  28. }

测试

访问登录页面:http://localhost:9999/login.html
访问首页:http://localhost:9999/index

HttpBasic模式登录认证

引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>

编写配置类

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. http.httpBasic()//开启httpbasic认证
  6. .and()
  7. .authorizeRequests()
  8. .anyRequest()
  9. .authenticated();//所有请求都需要登录认证才能访问
  10. }
  11. }

测试

运行 main 方法,访问页面此时我们会发现,之前能访问的页面都需要输入用户名、密码才能访问了
用户名为:user,密码在控制台打出:
image.png

自定义用户名密码

我们可以在 application.yml 文件中定义用户名和密码

  1. spring:
  2. security:
  3. user:
  4. name: admin
  5. password: admin

重新运行 main 方法,然后使用上述定义好的用户名密码试试

formLogin登录认证模式

引入依赖

我们在之前步骤已经引入过了该依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>

删除自定义用户名、密码

我们在 HttpBasic 模式登录认证的时候,有在 application.yml 文件中定义用户名、密码。

  1. spring:
  2. security:
  3. user:
  4. name: admin
  5. password: admin

配置类

以下代码是修改后的配置类。主要做的几件事
①、加载用户信息到内存。我们这次使用的是硬编码,加载用户以及用户的角色、权限到内存。
②、往容器中注入密码的加密和校验的接口实现类。
③、定义了登录认证的逻辑(即接收登录参数、登录成功后逻辑)
④、定义了资源访问控制逻辑,即访问哪些资源,需要什么样的权限

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. http.csrf().disable() //禁用跨站csrf攻击防御,后面的章节会专门讲解
  6. .formLogin()
  7. .loginPage("/login.html")//一旦用户的请求没有权限就跳转到这个页面
  8. .loginProcessingUrl("/login")//登录表单form中action的地址,也就是处理认证请求的路径
  9. .usernameParameter("username")///登录表单form中用户名输入框input的name名,不修改的话默认是username
  10. .passwordParameter("password")//form中密码输入框input的name名,不修改的话默认是password
  11. .defaultSuccessUrl("/")//登录认证成功后默认转跳的路径
  12. .and()
  13. .authorizeRequests()
  14. .antMatchers("/login.html","/login").permitAll()//不需要通过登录验证就可以被访问的资源路径
  15. .antMatchers("/","/biz1","/biz2") //资源路径匹配
  16. .hasAnyAuthority("ROLE_user","ROLE_admin") //user角色和admin角色都可以访问
  17. .antMatchers("/syslog","/sysuser") //资源路径匹配
  18. .hasAnyRole("admin") //admin角色可以访问
  19. //.antMatchers("/syslog").hasAuthority("sys:log")
  20. //.antMatchers("/sysuser").hasAuthority("sys:user")
  21. .anyRequest().authenticated();
  22. }
  23. @Override
  24. public void configure(WebSecurity web) {
  25. //将项目中静态资源路径开放出来
  26. web.ignoring().antMatchers( "/css/**", "/fonts/**", "/img/**", "/js/**");
  27. }
  28. @Override
  29. public void configure(AuthenticationManagerBuilder auth) throws Exception {
  30. auth.inMemoryAuthentication()
  31. .withUser("user")
  32. .password(passwordEncoder().encode("123456"))
  33. .roles("user")
  34. .and()
  35. .withUser("admin")
  36. .password(passwordEncoder().encode("123456"))
  37. //.authorities("sys:log","sys:user")
  38. .roles("admin")
  39. .and()
  40. .passwordEncoder(passwordEncoder());//配置BCrypt加密
  41. }
  42. @Bean
  43. public PasswordEncoder passwordEncoder(){
  44. return new BCryptPasswordEncoder();
  45. }
  46. }

测试

重新运行 main 方法,访问其他页面,都会被跳转到登录页面
使用用户名(user/123456)登录,登录到首页,发现业务1、业务2正常访问。而日志管理、用户管理访问报错,如下
image.png
使用用户名(admin/123456),登录后可以访问所有资源。

自定义权限访问异常信息处理

当我们访问资源的时候,可能会出现这2种情况。
情况一: 未登录,直接访问资源。默认情况会跳转到登录页面。
情况二:登录系统,访问某些资源没有权限,默认情况会返回 403 的错误页面

解决方法:
情况一:返回 JSON 数据,告知需要先登录才能访问
情况二:返回 JSON 数据,告知权限不足

定义返回消息

  1. public class AjaxResult extends HashMap<String, Object> {
  2. private static final long serialVersionUID = 1L;
  3. /**
  4. * 状态码
  5. */
  6. public static final String CODE_TAG = "code";
  7. /**
  8. * 返回内容
  9. */
  10. public static final String MSG_TAG = "msg";
  11. /**
  12. * 数据对象
  13. */
  14. public static final String DATA_TAG = "data";
  15. /**
  16. * 状态类型
  17. */
  18. public enum Type {
  19. /**
  20. * 成功
  21. */
  22. SUCCESS(0),
  23. /**
  24. * 警告
  25. */
  26. WARN(301),
  27. /**
  28. * 错误
  29. */
  30. ERROR(500);
  31. private final int value;
  32. Type(int value) {
  33. this.value = value;
  34. }
  35. public int value() {
  36. return this.value;
  37. }
  38. }
  39. /**
  40. * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
  41. */
  42. public AjaxResult() {
  43. }
  44. /**
  45. * 初始化一个新创建的 AjaxResult 对象
  46. *
  47. * @param type 状态类型
  48. * @param msg 返回内容
  49. */
  50. public AjaxResult(Type type, String msg) {
  51. super.put(CODE_TAG, type.value);
  52. super.put(MSG_TAG, msg);
  53. }
  54. /**
  55. * 初始化一个新创建的 AjaxResult 对象
  56. *
  57. * @param type 状态类型
  58. * @param msg 返回内容
  59. * @param data 数据对象
  60. */
  61. public AjaxResult(Type type, String msg, Object data) {
  62. super.put(CODE_TAG, type.value);
  63. super.put(MSG_TAG, msg);
  64. if (null != data && data != "") {
  65. super.put(DATA_TAG, data);
  66. }
  67. }
  68. /**
  69. * 方便链式调用
  70. *
  71. * @param key 键
  72. * @param value 值
  73. * @return 数据对象
  74. */
  75. @Override
  76. public AjaxResult put(String key, Object value) {
  77. super.put(key, value);
  78. return this;
  79. }
  80. /**
  81. * 返回成功消息
  82. *
  83. * @return 成功消息
  84. */
  85. public static AjaxResult success() {
  86. return AjaxResult.success("操作成功");
  87. }
  88. /**
  89. * 返回成功数据
  90. *
  91. * @return 成功消息
  92. */
  93. public static AjaxResult success(Object data) {
  94. return AjaxResult.success("操作成功", data);
  95. }
  96. /**
  97. * 返回成功消息
  98. *
  99. * @param msg 返回内容
  100. * @return 成功消息
  101. */
  102. public static AjaxResult success(String msg) {
  103. return AjaxResult.success(msg, null);
  104. }
  105. /**
  106. * 返回成功消息
  107. *
  108. * @param msg 返回内容
  109. * @param data 数据对象
  110. * @return 成功消息
  111. */
  112. public static AjaxResult success(String msg, Object data) {
  113. return new AjaxResult(Type.SUCCESS, msg, data);
  114. }
  115. /**
  116. * 返回警告消息
  117. *
  118. * @param msg 返回内容
  119. * @return 警告消息
  120. */
  121. public static AjaxResult warn(String msg) {
  122. return AjaxResult.warn(msg, null);
  123. }
  124. /**
  125. * 返回警告消息
  126. *
  127. * @param msg 返回内容
  128. * @param data 数据对象
  129. * @return 警告消息
  130. */
  131. public static AjaxResult warn(String msg, Object data) {
  132. return new AjaxResult(Type.WARN, msg, data);
  133. }
  134. /**
  135. * 返回错误消息
  136. *
  137. * @return
  138. */
  139. public static AjaxResult error() {
  140. return AjaxResult.error("操作失败");
  141. }
  142. /**
  143. * 返回错误消息
  144. *
  145. * @param msg 返回内容
  146. * @return 警告消息
  147. */
  148. public static AjaxResult error(String msg) {
  149. return AjaxResult.error(msg, null);
  150. }
  151. /**
  152. * 返回错误消息
  153. *
  154. * @param msg 返回内容
  155. * @param data 数据对象
  156. * @return 警告消息
  157. */
  158. public static AjaxResult error(String msg, Object data) {
  159. return new AjaxResult(Type.ERROR, msg, data);
  160. }
  161. }

引入 fastjson

  1. <dependency>
  2. <groupId>com.alibaba</groupId>
  3. <artifactId>fastjson</artifactId>
  4. <version>1.2.76</version>
  5. </dependency>

定义匿名访问的处理类

  1. public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
  2. @Override
  3. public void commence(HttpServletRequest request, HttpServletResponse response,
  4. AuthenticationException authException) throws IOException, ServletException {
  5. response.setCharacterEncoding("utf-8");
  6. response.setContentType("text/javascript;charset=utf-8");
  7. response.getWriter().print(JSONObject.toJSONString(AjaxResult.error("未登录,无法直接访问资源,请先登陆!")));
  8. }
  9. }

定义没有权限访问的处理类

  1. public class CustomAccessDeineHandler implements AccessDeniedHandler {
  2. @Override
  3. public void handle(HttpServletRequest request, HttpServletResponse response,
  4. AccessDeniedException accessDeniedException) throws IOException, ServletException {
  5. response.setCharacterEncoding("utf-8");
  6. response.setContentType("text/javascript;charset=utf-8");
  7. response.getWriter().print(JSONObject.toJSONString(AjaxResult.error("权限不足,无法访问!")));
  8. }
  9. }

修改配置类

在 SpringSecurity的配置类中,添加自定义权限的处理
image.png

同账号多端登录踢下线

描述:
例如账号 user,已经登陆了。此时该账号再别处再次登录时会将原来登录的账号踢出下线。

处理策略

此处提供2种策略方案。
第一种是针对 非前后端分离项目的,也就是当该账号再别处登录时,之前登录的账号再刷新后会跳转到登陆页面

第二中是针对 前后端分离项目,当别处登录时,返回 JSON 数据提示用户。

方案一

当账号已在别处登录时,原登录账号再刷新页面时重定向到登录页面

  1. public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
  2. //页面跳转的处理逻辑
  3. private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
  4. @Override
  5. public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
  6. // 是跳转html页面,url代表跳转的地址
  7. redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "/login.html");
  8. }
  9. }

上面策略的代码只是做了页面重定向,如果想要在重定向之前弹窗提示用户,可以这么做:

  1. public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
  2. //页面跳转的处理逻辑
  3. //private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
  4. @Override
  5. public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
  6. HttpServletResponse response = event.getResponse();
  7. response.setContentType("text/html;charset=UTF-8");
  8. response.setCharacterEncoding("UTF-8");
  9. PrintWriter out = response.getWriter();
  10. out.println("<script>");
  11. out.println("alert('该账号已在别处登录,请重新登录!');");
  12. out.println("location.href='/login.html'");
  13. out.println("</script>");
  14. // 是跳转html页面,url代表跳转的地址
  15. // redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "/login.html");
  16. }
  17. }

方案二

当账号已在别处登录时,原登录账号再刷新页面时 会得到 JSON数据提示

  1. public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
  2. //jackson的JSON处理对象
  3. private ObjectMapper objectMapper = new ObjectMapper();
  4. @Override
  5. public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
  6. Map<String, Object> map = new HashMap<>();
  7. map.put("code", 403);
  8. map.put("msg", "您的登录已经超时或者已经在另一台机器登录,您被迫下线。"
  9. + event.getSessionInformation().getLastRequest());
  10. // Map -> Json
  11. String json = objectMapper.writeValueAsString(map);
  12. //输出JSON信息的数据
  13. event.getResponse().setContentType("application/json;charset=UTF-8");
  14. event.getResponse().getWriter().write(json);
  15. }
  16. }

配置策略

image.png

测试

重新运行 main 方法, 使用 user/123456 账号分别在 谷歌 和 火狐浏览器上登录,验证是否会别踢出下线!!

退出功能实现

简单实现

在首页添加如下代码:

  1. <a href="/logout" >退出</a>

在 SpringSecurity 的配置类中添加退出配置

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. ....省略....
  4. http.logout();
  5. }

测试:
重新运行 main ,使用 user / 123456 登录系统中 index.html 页面,点击页面上 退出按钮后,即将跳转到登录页面

SpringSecurity退出功能默认做了哪些事

  1. 当前session失效,即:logout的核心需求,session失效就是访问权限的回收。
  2. 删除当前用户的 remember-me“记住我”功能信息
  3. clear清除当前的 SecurityContext
  4. 重定向到登录页面,loginPage配置项指定的页面

定制退出功能

我们可以对退出功能做一些个性化配置,如下:

  1. http.logout()
  2. .logoutUrl("/signout")
  3. .logoutSuccessUrl("/aftersignout.html")
  4. .deleteCookies("JSESSIONID")

如果还嫌不够的话,我们还可以实现 LogoutSuccessHandler 接口,来对我们退出功能进行深度化定制。

  1. @Component
  2. public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
  3. @Override
  4. public void onLogoutSuccess(HttpServletRequest request,
  5. HttpServletResponse response,
  6. Authentication authentication)
  7. throws IOException, ServletException {
  8. //这里书写你自己的退出业务逻辑
  9. // 重定向到登录页
  10. response.sendRedirect("/login.html");
  11. }
  12. }
  13. ======================================================================================
  14. @Configuration
  15. @EnableWebSecurity
  16. public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
  17. @Autowired
  18. private MyLogoutSuccessHandler myLogoutSuccessHandler;
  19. @Override
  20. protected void configure(final HttpSecurity http) throws Exception {
  21. http.logout()
  22. .logoutUrl("/signout")
  23. //.logoutSuccessUrl(``"/aftersignout.html"``)
  24. .deleteCookies("JSESSIONID")
  25. //自定义logoutSuccessHandler
  26. .logoutSuccessHandler(myLogoutSuccessHandler);
  27. }
  28. }

记住我功能实现

简单实现

登录表单

在登陆表单中添加记住我的复选框

  1. <label><input type="checkbox" name="remember-me"/>记住密码</label>

配置记住我的功能

在 SpringSecurity的配置文件中,添加记住我的配置 http.rememberMe();

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. http.rememberMe(); //实现记住我自动登录配置,核心的代码只有这一行
  6. }
  7. }

测试

重新运行 main 方法,先登录,然后重启浏览器,再次直接访问系统资源,发现不需要再次登录。

实现原理

  1. 当我们登陆的时候,除了用户名、密码,我们还可以勾选remember-me。
  2. 如果我们勾选了remember-me,当我们登录成功之后服务端会生成一个Cookie返回给浏览器,这个Cookie的名字默认是remember-me;值是一个token令牌。
  3. 当我们在有效期内再次访问应用时,经过RememberMeAuthenticationFilter,读取Cookie中的token进行验证。验正通过不需要再次登录就可以进行应用访问

个性化设置

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. http.rememberMe()
  6. .rememberMeParameter("remember-me-new")
  7. .rememberMeCookieName("remember-me-cookie")
  8. .tokenValiditySeconds(2 * 24 * 60 * 60);
  9. }
  10. }

设置了http.rememberMe() .rememberMeParameter(“remember-me-new”) 我们需要修改记住我复选框中的name属性值

Token令牌持久化功能

经过上述步骤,我们已经实现了记住我功能,但现在存在一个问题。就是 token 令牌与用户的对应关系 这些数据是存放在内存中的。也就是说当 我们的应用重启后,用户需要重新登录系统。

如何解决?
我们需要将用户和 token令牌的对应关系数据,存放到数据库中进行持久化即可。

引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>mysql</groupId>
  7. <artifactId>mysql-connector-java</artifactId>
  8. </dependency>

主配置文件

在 application.yml 文件中,添加数据源基本信息

  1. server:
  2. port: 9999
  3. # 数据库连接基本信息
  4. spring:
  5. datasource:
  6. url: jdbc:mysql://localhost:3306/security-demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
  7. driver-class-name: com.mysql.cj.jdbc.Driver
  8. username: root
  9. password: 123456

创建数据库

创建 security-demo 的数据库,字符编码 utf8mb4

创建表

  1. CREATE TABLE `persistent_logins` (
  2. `username` varchar(64) NOT NULL,
  3. `series` varchar(64) NOT NULL,
  4. `token` varchar(64) NOT NULL,
  5. `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  6. PRIMARY KEY (`series`)
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

修改配置类

在 SpringSecurity的配置类中,我们需要注入 PersistentTokenRepository 类型的bean。同时需要对令牌持久化进行配置
image.png
image.png

测试

重新运行 main 方法,使用账号密码登录系统,然后重新运行main方法,此时发现在访问系统时不需要重新登录
另外,在数据库表中出现如下数据:
image.png