认证配置
表单认证
注销登录
前后端分离认证
添加验证码
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
/index:主页,公共资源,可以直接访问
/hello .... :受保护资源,需要权限管理(认证授权之后才能访问),默认所有资源都是受保护的,需要认证授权之后才能访问
在项目中添加如下配置就可以实现对资源权限规则设定:
@Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override /** * 说明: SpringBoot2.6.2版本 * permitAll() 代表放行该资源,该资源为公共资源,无需认证和授权可以直接访问 * anyRequest().authenticated() 代表所有请求,必须认证之后才能访问 * formLogin() 代表开启表单认证 * 注意: 放行资源必须放在所有认证请求之前! */ protected void configure(HttpSecurity http) throws Exception { http.authorizeHttpRequests() .mvcMatchers("/index").permitAll() //放行资源写在任何前面 .anyRequest().authenticated() .and().formLogin(); } }
注意:在SpringBoot 2.7.1 中的spring-security-config-5.7.2.RELEASE中已提到WebSecurityConfigurerAdapter已过时被弃用,替代方法如下:
使用 SecurityFilterChain Bean 来配置 HttpSecurity;
使用 WebSecurityCustomizer Bean 来配置 WebSecurity。
参考链接:
SpringSecurity - WebSecurityConfigurerAdapter 过时问题_爱上云原生的博客-CSDN博客
Spring Security即将弃用配置类WebSecurityConfigurerAdapter_码农小胖哥的博客-CSDN博客
替代后新代码为:
package com.study.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; /** * @ClassName WebSecurityConfig * @Description 自定义资源权限规则 * @Author Jiangnan Cui * @Date 2022/7/6 12:23 * @Version 1.0 */ @Configuration @EnableWebSecurity //添加security过滤器,此处一定要加入此注解,否则下面的httpSecurity无法装配 public class WebSecurityConfig { /** * 说明: SpringBoot2.7.1版本 * permitAll() 代表放行该资源,该资源为公共资源,无需认证和授权可以直接访问 * anyRequest().authenticated() 代表所有请求,必须认证之后才能访问 * formLogin() 代表开启表单认证 * 注意: 放行资源必须放在所有认证请求之前! */ @Bean SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity .authorizeHttpRequests() .mvcMatchers("/index").permitAll() .anyRequest().authenticated() .and().formLogin().and().build(); } }
总结:新替代方案中Spring Security 就不需要再去继承WebSecurityConfigurerAdapter,然后重写 configure()方法了,直接通过 filterChain() 方法就能使用 HttpSecurity 来配置相关信息。
重启项目,测试访问index请求时直接放行,访问hello请求时需要登录后才能正常访问!!!
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
pom.xml引入Thymeleaf模板依赖
org.springframework.boot spring-boot-starter-thymeleaf
application.properties配置Thymeleaf
# Thymeleaf配置 # 关闭缓存 spring.thymeleaf.cache=false # 默认前缀路径,可不配置 spring.thymeleaf.prefix=classpath:/templates/ # 默认文件后缀,可不配置 spring.thymeleaf.suffix=.html
定义登录页面LoginController
package com.study.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @ClassName LoginController * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/23 17:47 * @Version 1.0 */ @Controller public class LoginController { @RequestMapping("/login.html") //注意此处是login.html而不是login public String login() { return "login";//封装为login.html } }
在 templates 中定义登录界面login.html
登录页面 用户登录
需要注意的是:
登录表单 method 必须为 post
,action 的请求路径可以定义为 /doLogin
用户名的 name 属性为 username
,用户名可以指定为除username外的其他名称
密码的 name 属性为 password
,密码可以指定为除password外的其他名称
配置 Spring Security 配置类WebSecurityConfigurer
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @ClassName WebSecurityConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/21 22:34 * @Version 1.0 */ @Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .mvcMatchers("/index").permitAll() //放行/index请求 .mvcMatchers("/login.html").permitAll() //放行/login.html请求 .anyRequest().authenticated() //其它请求需要登录认证后才能访问 .and() .formLogin() //默认form表单页面登录 .loginPage("/login.html") //使用自定义登录页面登录页面登录 .loginProcessingUrl("/doLogin") //使用自定义登录页面时需要重新指定url,对应login.html中的action路径 .usernameParameter("uname") //重新指定用户名名称 .passwordParameter("pwd") //重新指定密码名称 .successForwardUrl("/index") //认证成功后跳转路径 //forward 跳转路径 始终在认证成功之后跳转到指定请求 地址栏不变 //.defaultSuccessUrl("/hello") //默认认证成功后跳转路径 //.defaultSuccessUrl("/hello",true) //第二个参数设置为true时总是跳转,效果同successForwardUrl一致,默认false //redirect 重定向 注意:如果之前有请求过的路径,会优先跳转之前的请求路径 地址栏改变 .failureUrl("/login.html") //登录失败后跳转路径 .and() .csrf().disable();//此处先关闭CSRF跨站保护 } }
注意:successForwardUrl 、defaultSuccessUrl 这两个方法都可以实现成功之后跳转,其中:
successForwardUrl 默认使用 forward
跳转 注意:不会跳转到之前请求路径
defaultSuccessUrl 默认使用 redirect
跳转 注意:如果之前请求路径,会有优先跳转之前请求路径,可以传入第二个参数进行修改
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面,只需要给前端返回一个 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 Authentication object which was created during * the authentication process. */ void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException; }
根据接口的描述信息也可以得知,登录成功后会自动回调这个方法,进一步查看它的默认实现,你会发现successForwardUrl、defaultSuccessUrl也是由它的子类实现的:
在config包中自定义 AuthenticationSuccessHandler接口实现类MyAuthenticationSuccessHandler
package com.study.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * @ClassName MyAuthenticationSuccessHandler * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/23 20:30 * @Version 1.0 */ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Mapresult = new HashMap (); result.put("msg", "登录成功");//打印登录成功信息 result.put("status", 200);//打印状态码 result.put("authentication", authentication);//打印认证信息 response.setContentType("application/json;charset=UTF-8");//设置响应类型 String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串 response.getWriter().println(s);//打印json格式数据 } }
在WebSecurityConfigurer中配置MyAuthenticationSuccessHandler
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @ClassName WebSecurityConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/21 22:34 * @Version 1.0 */ @Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { ...... //.successForwardUrl("/index") //认证成功后跳转路径 forward 跳转路径 始终在认证成功之后跳转到指定请求 地址栏不变 //.defaultSuccessUrl("/hello") //默认认证成功后跳转路径 redirect 重定向 注意:如果之前有请求过的路径,会优先跳转之前的请求路径 地址栏改变 //.defaultSuccessUrl("/hello",true) //第二个参数设置为true时总是跳转,效果同successForwardUrl一致,默认false .successHandler(new MyAuthenticationSuccessHandler())//认证成功时处理,前后端分离解决方案 .and() .csrf().disable();//此处先关闭CSRF跨站保护 } }
访问路径:http://localhost:8080/hello,输入用户名密码登录后,输出结果:
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
为了能更直观地在登录页面看到异常错误信息,可以在登录页面中直接获取异常信息。Spring Security 在登录失败之后会将异常信息存储到 request
、session
作用域中key为 SPRING_SECURITY_LAST_EXCEPTION
命名属性中,源码可以参考 SimpleUrlAuthenticationFailureHandler :
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.springframework.security.web.authentication; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.util.UrlUtils; import org.springframework.util.Assert; public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler { protected final Log logger = LogFactory.getLog(this.getClass()); private String defaultFailureUrl; private boolean forwardToDestination = false; private boolean allowSessionCreation = true; private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); public SimpleUrlAuthenticationFailureHandler() { } public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) { this.setDefaultFailureUrl(defaultFailureUrl); } public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { if (this.defaultFailureUrl == null) { if (this.logger.isTraceEnabled()) { this.logger.trace("Sending 401 Unauthorized error since no failure URL is set"); } else { this.logger.debug("Sending 401 Unauthorized error"); } response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } else { this.saveException(request, exception); if (this.forwardToDestination) { this.logger.debug("Forwarding to " + this.defaultFailureUrl); request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response); } else { this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl); } } } protected final void saveException(HttpServletRequest request, AuthenticationException exception) { if (this.forwardToDestination) { request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception); } else { HttpSession session = request.getSession(false); if (session != null || this.allowSessionCreation) { request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception); } } } public void setDefaultFailureUrl(String defaultFailureUrl) { Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl), () -> { return "'" + defaultFailureUrl + "' is not a valid redirect URL"; }); this.defaultFailureUrl = defaultFailureUrl; } protected boolean isUseForward() { return this.forwardToDestination; } public void setUseForward(boolean forwardToDestination) { this.forwardToDestination = forwardToDestination; } public void setRedirectStrategy(RedirectStrategy redirectStrategy) { this.redirectStrategy = redirectStrategy; } protected RedirectStrategy getRedirectStrategy() { return this.redirectStrategy; } protected boolean isAllowSessionCreation() { return this.allowSessionCreation; } public void setAllowSessionCreation(boolean allowSessionCreation) { this.allowSessionCreation = allowSessionCreation; } }
login.html中添加显示异常信息代码
登录页面 用户登录
...
WebSecurityConfigurer配置登录失败处理
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @ClassName WebSecurityConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/21 22:34 * @Version 1.0 */ @Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ....... .successHandler(new MyAuthenticationSuccessHandler())//认证成功时处理,前后端分离解决方案 //.failureForwardUrl("/login.html")//认证失败之后,forward跳转 .failureUrl("/login.html") //默认认证失败之后,redirect跳转 .and() .csrf().disable();//此处先关闭CSRF跨站保护 } }
注意:failureUrl、failureForwardUrl 关系类似于之前提到的 successForwardUrl 、defaultSuccessUrl 方法
failureUrl:失败以后的重定向跳转
failureForwardUrl:失败以后的 forward 跳转
注意:因此获取 request 中异常信息,这里只能使用failureForwardUrl
启动项目,测试路径:http://localhost:8080/hello,输入错误的用户名和密码时会输出如下错误信息,注意login.html与WebSecurityConfigurer对failureForwardUrl或failureUrl的配置一致:
(1)failureForwardUrl:forward跳转,request作用域
(2)failureUrl:redirect跳转,session作用域
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
和自定义登录成功处理一样,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也是由它的子类实现的。
config包中自定义 AuthenticationFailureHandler接口实现类MyAuthenticationFailureHandler
package com.study.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * @ClassName MyAuthenticationFailureHandler * @Description 自定义认证失败处理 * @Author Jiangnan Cui * @Date 2022/7/24 15:54 * @Version 1.0 */ public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { Mapresult = new HashMap (); 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); } }
WebSecurityConfigurer配置MyAuthenticationFailureHandler
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @ClassName WebSecurityConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/21 22:34 * @Version 1.0 */ @Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ....... //.failureForwardUrl("/login.html")//认证失败之后,forward跳转 //.failureUrl("/login.html") //默认认证失败之后,redirect跳转 .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案 .and() .csrf().disable();//此处先关闭CSRF跨站保护 } }
测试路径:http://localhost:8080/hello,输入错误的用户名或密码后输出如下结果:
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
Spring Security 中也提供了默认的注销登录配置,在开发时也可以按照自己需求对注销进行个性化定制。
开启注销登录(默认开启)
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; /** * @ClassName WebSecurityConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/21 22:34 * @Version 1.0 */ @Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ....... .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案 .and() .logout() .logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET .invalidateHttpSession(true)//默认开启会话失效 .clearAuthentication(true)//默认清除认证标志 .logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面 .and() .csrf().disable();//此处先关闭CSRF跨站保护 } }
测试注销路径:登录成功后,访问:http://localhost:8080/hello,通过访问:http://localhost:8080/logout,进行注销登录。
注意:
通过 logout() 方法开启注销配置
logoutUrl 指定退出登录请求地址,默认是 GET 请求,路径为 /logout
invalidateHttpSession 退出时是否使session 失效,默认值为 true
clearAuthentication 退出时是否清除认证信息,默认值为 true
logoutSuccessUrl 退出登录时跳转地址
配置多个注销登录请求
如果项目中有需要,开发者还可以配置多个注销登录的请求,同时还可以指定请求的方法:
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; /** * @ClassName WebSecurityConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/21 22:34 * @Version 1.0 */ @Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ....... .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案 .and() .logout() //.logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET .logoutRequestMatcher(new OrRequestMatcher( new AntPathRequestMatcher("/aaa", "GET"), new AntPathRequestMatcher("/bbb", "POST") )) .invalidateHttpSession(true)//默认开启会话失效 .clearAuthentication(true)//默认清除认证标志 .logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面 .and() .csrf().disable();//此处先关闭CSRF跨站保护 } }
注意:
GET请求直接在地址栏访问,注销登录路径:http://localhost:8080/aaa
POST方式需要新建表单及对应的Controller,注销登录路径:http://localhost:8080/logout.html
logout.html
注销页面 用户注销
LogoutController
package com.study.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @ClassName LogoutController * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/24 18:15 * @Version 1.0 */ @Controller public class LogoutController { @RequestMapping("logout.html") public String logout() { return "logout"; } }
前后端分离注销登录配置
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
如果是前后端分离开发,注销成功之后就不需要页面跳转了,只需要将注销成功的信息返回前端即可,此时我们可以通过自定义 LogoutSuccessHandler 实现来返回注销之后信息:
编写LogoutSuccessHandler接口实现类MyLogoutSuccessHandler
package com.study.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * @ClassName MyLogoutSuccessHandler * @Description 自定义认证注销处理 * @Author Jiangnan Cui * @Date 2022/7/23 20:30 * @Version 1.0 */ public class MyLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Mapresult = new HashMap (); result.put("msg", "注销成功,当前认证对象为:" + authentication);//打印认证信息 result.put("status", 200);//打印状态码 response.setContentType("application/json;charset=UTF-8");//设置响应类型 String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串 response.getWriter().println(s);//打印json格式数据 } }
WebSecurityConfigurer配置MyLogoutSuccessHandler
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; /** * @ClassName WebSecurityConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/21 22:34 * @Version 1.0 */ @Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ....... .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案 .and() .logout() //.logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET .logoutRequestMatcher(new OrRequestMatcher( new AntPathRequestMatcher("/aaa", "GET"), new AntPathRequestMatcher("/bbb", "POST") )) .invalidateHttpSession(true)//默认开启会话失效 .clearAuthentication(true)//默认清除认证标志 //.logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面 .logoutSuccessHandler(new MyLogoutSuccessHandler()) .and() .csrf().disable();//此处先关闭CSRF跨站保护 } }
测试路径:http://localhost:8080/aaa
测试路径:http://localhost:8080/logout.html
配套视频:18.获取用户认证信息_哔哩哔哩_bilibili
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 default strategyName = 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; } //..... } }
三种策略详细解释:
MODE THREADLOCAL
:这种存放策略是将 SecurityContext 存放在 ThreadLocal中,大家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达 Servlet,都是由一个线程来处理的,这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。
MODE INHERITABLETHREADLOCAL
:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么就可以使用这种存储模式,使用时需要在VM Options单独进行配置(实际上是将父线程的用户数据复制一份到子线程)。
MODE GLOBAL
:这种存储模式实际上是将数据保存在一个静态变量中,在 JavaWeb开发中,这种模式很少使用到(了解即可)。
通过 SecurityContextHolder 可以得知,SecurityContextHolderStrategy 接口用来定义存储策略方法:
public interface SecurityContextHolderStrategy { void clearContext(); SecurityContext getContext(); void setContext(SecurityContext context); SecurityContext createEmptyContext(); }
接口中一共定义了四个方法:
clearContext
:该方法用来清除存储的 SecurityContext对象。
getContext
:该方法用来获取存储的 SecurityContext 对象。
setContext
:该方法用来设置存储的 SecurityContext 对象。
createEmptyContext
:该方法则用来创建一个空的 SecurityContext 对象。
@RestController public class HelloController { @RequestMapping("/hello") public String hello() { System.out.println("Hello Spring Security!"); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//获得认证信息 System.out.println("authentication.getName() = " + authentication.getName()); System.out.println("authentication.getAuthorities() = " + authentication.getAuthorities()); System.out.println("authentication.getCredentials() = " + authentication.getCredentials()); System.out.println("authentication.getDetails() = " + authentication.getDetails()); System.out.println("authentication.getPrincipal() = " + authentication.getPrincipal()); User principal = (User) authentication.getPrincipal();//返回值类型为User时需要进行强制转换 org.springframework.security.core.userdetails.User System.out.println("principal.getUsername() = " + principal.getUsername()); System.out.println("principal.getPassword() = " + principal.getPassword()); System.out.println("principal.getAuthorities() = " + principal.getAuthorities()); return "hello spring security!"; } }
启动服务,登录成功后访问:http://localhost:8080/hello,获得认证之后的用户数据。
IDEA控制台输出结果:
Hello Spring Security! authentication.getName() = root authentication.getAuthorities() = [ROLE_admin, ROLE_super] authentication.getCredentials() = null authentication.getDetails() = WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=15FEA49E4810C435286FD8B6D188C87F] authentication.getPrincipal() = org.springframework.security.core.userdetails.User [Username=root, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_admin, ROLE_super]] principal.getUsername() = root principal.getPassword() = null principal.getAuthorities() = [ROLE_admin, ROLE_super]
package com.study.controller; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @ClassName HelloController * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/6 12:12 * @Version 1.0 */ @RestController public class HelloController { @RequestMapping("/hello") public String hello() { System.out.println("Hello Spring Security!"); new Thread(() -> { Authentication Authentication = SecurityContextHolder.getContext().getAuthentication(); User childPrincipal = (User) Authentication.getPrincipal(); System.out.println("Principal.getUsername() = " + Principal.getUsername()); System.out.println("Principal.getPassword() = " + Principal.getPassword()); System.out.println("Principal.getAuthorities() = " + Principal.getAuthorities()); }).start(); return "hello spring security!"; } }
启动服务,访问:http://localhost:8080/hello,获得认证之后的用户数据。
视频中报错:
实际测试报错:
Exception in thread "Thread-2" java.lang.NullPointerException at com.study.controller.HelloController.lambda$hello$0(HelloController.java:35) at java.lang.Thread.run(Thread.java:750)
从上面可以看到默认策略,是无法在子线程中获取用户信息,如果需要在子线程中获取必须使用第二种策略,默认策略是通过 System.getProperty 加载的,因此我们可以通过增加 VM Options 参数进行修改。
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
启动服务,访问:http://localhost:8080/hello,获得认证之后的用户数据。
IDEA控制台输出结果:
Hello Spring Security! Principal.getUsername() = root Principal.getPassword() = null Principal.getAuthorities() = [ROLE_admin, ROLE_super]
配套视频:19.页面中获取用户认证信息_哔哩哔哩_bilibili
pom.xml引入依赖
org.thymeleaf.extras thymeleaf-extras-springsecurity5 3.0.4.RELEASE
logout.html页面加入命名空间
logout.html页面中获取用户信息
启动服务,登录成功后访问logout.html:http://localhost:8080/logout.html
配套视频:20.自定义数据源之认证流程分析_哔哩哔哩_bilibili
Servlet Authentication Architecture :: Spring Security
发起认证请求,请求中携带用户名、密码,该请求会被UsernamePasswordAuthenticationFilter
拦截
在UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法中将请求中用户名和密码,封装为Authentication
对象,并交给AuthenticationManager
进行认证
认证成功,将认证信息存储到 SecurityContextHodler 以及调用记住我等,并回调 AuthenticationSuccessHandler
处理
认证失败,清除 SecurityContextHodler 以及 记住我中信息,回调 AuthenticationFailureHandler
处理
从上面分析中得知,AuthenticationManager 是认证的核心类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider 。他们三者关系是样的呢?
AuthenticationManager
:是一个认证管理器,它定义了 Spring Security 过滤器要执行认证操作。
ProviderManager
:AuthenticationManager接口的实现类,Spring Security 认证时默认使用就是的 ProviderManager。
AuthenticationProvider
:就是针对不同的身份类型执行的具体的身份认证。
(1)AuthenticationManager 与 ProviderManager:父子关系
ProviderManager 是 AuthenticationManager 的唯一实现(其它实现要么是私有的,要么是静态的,不能被使用),也是 Spring Security 默认使用的实现。从这里不难看出,默认情况下AuthenticationManager 就是一个ProviderManager。
(2)ProviderManager 与 AuthenticationProvider
摘自官方: Servlet Authentication Architecture :: Spring Security
在 Spring Security 中,允许系统同时支持多种不同的认证方式,例如同时支持用户名/密码认证、RememberMe 认证、手机号码动态认证等,而不同的认证方式对应了不同的 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 关系:
摘自官网: Topical Guide | Spring Security Architecture
配套视频:21.自定义数据源之原理分析 (一)_哔哩哔哩_bilibili
弄清楚认证原理之后我们来看下具体认证时数据源的获取:默认情况下,AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过UserDetailsService完成数据源的校验。
他们之间调用关系如下:
总结: AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式,认证时底层数据源需要调用 UserDetailService 来实现。
配套视频:22.自定义数据源之原理分析(二)_哔哩哔哩_bilibili
23.自定义数据源之全局配置AuthenticationManager方式_哔哩哔哩_bilibili
官方网站:Topical Guide | Spring Security Architecture
默认的全局 AuthenticationManager:springboot 对 security 进行自动配置时,自动在工厂中创建一个全局AuthenticationManager
//springboot对security默认配置中,在工厂中默认创建AuthenticationManager @Autowired //此段代码是从官网复制来的 public void initialize(AuthenticationManagerBuilder builder) throws Exception { System.out.println("springboot默认配置: " + builder); } /** * 输出结果: * springboot默认配置: org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@60297f36 */
总结:默认自动配置创建的全局AuthenticationManager
默认查找当前项目中是否存在自定义的UserDetailService 实例,自动将当前项目UserDetailService 实例设置为数据源;
在工厂中使用时直接在代码中注入即可。
自定义全局 AuthenticationManager
@Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override public void configure(AuthenticationManagerBuilder builder) { //builder .... } }
总结:一旦通过 configure 方法自定义AuthenticationManager的实现
就会将工厂中自动配置的AuthenticationManager进行覆盖;
需要在实现中指定认证数据源对象UserDetaiService实例;
这种方式创建的AuthenticationManager对象是工厂内部本地的一个 AuthenticationManager对象,不允许在其他自定义组件中进行注入。
用来在工厂中暴露自定义AuthenticationManager 实例
@Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { //1.自定义AuthenticationManager 推荐使用这种方式,而不是直接使用默认自动配置的,但这种方式并没有在工厂中暴露出来 @Override public void configure(AuthenticationManagerBuilder builder) throws Exception { System.out.println("自定义AuthenticationManager: " + builder); builder.userDetailsService(userDetailsService()); } //作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入 @Override @Bean //记得加上此注解,将对象注入工厂 public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
测试代码总结:
package com.study.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; /** * @ClassName WebSecurityConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/21 22:34 * @Version 1.0 */ @Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { //1.默认AuthenticationManager:springboot对security默认配置中,在工厂中默认创建AuthenticationManager // @Autowired //代码从官网复制而来 // public void initialize(AuthenticationManagerBuilder builder) throws Exception { // System.out.println("springboot默认配置:" + builder); // builder.userDetailsService(userDetailsService()); // } /** * 未定义UserDetailsService前:启动服务,重新访问hello路径,发现原先的用户名、密码组合失效,需要使用master、123456才能登录成功 * 输出结果: * springboot默认配置:org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@68ace111 */ //在工厂中自定义userDetailsService @Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager(); userDetailsService.createUser(User.withUsername("master").password("{noop}123456").roles("admin").build());//noop表示明文 return userDetailsService; } /** * 定义UserDetailsService后启动服务测试结果: * 上面这段(1.默认AuthenticationManager:)默认可以不写,写的时候会报错: * org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'webSecurityConfigurer': * Requested bean is currently in creation: Is there an unresolvable circular reference? * 这段代码显得多余了 */ //2.自定义AuthenticationManager 推荐使用这种自定义方式,但并没有在工厂中进行暴露 @Override public void configure(AuthenticationManagerBuilder builder) throws Exception { System.out.println("自定义AuthenticationManager:" + builder); builder.userDetailsService(userDetailsService()); } /** * 启动服务,重新访问hello路径,发现原先的用户名、密码组合失效,需要使用master、123456才能登录成功 * 输出结果: * 自定义AuthenticationManager:org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$2@24fabd0f */ //作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入 @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .mvcMatchers("/index").permitAll() //放行/index请求 .mvcMatchers("/login.html").permitAll() //放行/login.html请求 .anyRequest().authenticated() //其它请求需要登录认证后才能访问 .and() .formLogin() //默认form表单页面登录 .loginPage("/login.html") //使用自定义登录页面登录页面登录 .loginProcessingUrl("/doLogin") //使用自定义登录页面时需要重新指定url,对应login.html中的action路径 .usernameParameter("uname") //重新指定用户名名称 .passwordParameter("pwd") //重新指定密码名称 //.successForwardUrl("/index") //认证成功后跳转路径 //forward 跳转路径 始终在认证成功之后跳转到指定请求 地址栏不变 //.defaultSuccessUrl("/hello") //默认认证成功后跳转路径 //.defaultSuccessUrl("/hello",true) //第二个参数设置为true时总是跳转,效果同successForwardUrl一致,默认false //redirect 重定向 注意:如果之前有请求过的路径,会优先跳转之前的请求路径 地址栏改变 .successHandler(new MyAuthenticationSuccessHandler())//认证成功时处理,前后端分离解决方案 //.failureForwardUrl("/login.html")//认证失败之后,forward跳转 //.failureUrl("/login.html") //默认认证失败之后,redirect跳转 .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案 .and() .logout() //.logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET .logoutRequestMatcher(new OrRequestMatcher( new AntPathRequestMatcher("/aaa", "GET"), new AntPathRequestMatcher("/bbb", "POST") )) .invalidateHttpSession(true)//默认开启会话失效 .clearAuthentication(true)//默认清除认证标志 //.logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面 .logoutSuccessHandler(new MyLogoutSuccessHandler()) .and() .csrf().disable();//此处先关闭CSRF跨站保护 } }
配套视频:24.自定义数据源之全局配置AuthenticationManager总结_哔哩哔哩_bilibili
通过上面分析可以重点在于UserDetailsService,查看UserDetailsService接口如下:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
里面只有一个要实现的方法loadUserByUsername,返回类型为UserDetails,查看UserDetails定义如下:
public interface UserDetails extends Serializable { Collection extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
通过在UserDetails上Ctrl+H可以在右侧查看其实现类User:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.springframework.security.core.userdetails; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; public class User implements UserDetails, CredentialsContainer { private static final long serialVersionUID = 560L; private static final Log logger = LogFactory.getLog(User.class); private String password; private final String username; private final Setauthorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; public User(String username, String password, Collection extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities); } public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection extends GrantedAuthority> authorities) { Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor"); this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); } public Collection getAuthorities() { return this.authorities; } public String getPassword() { return this.password; } public String getUsername() { return this.username; } public boolean isEnabled() { return this.enabled; } public boolean isAccountNonExpired() { return this.accountNonExpired; } public boolean isAccountNonLocked() { return this.accountNonLocked; } public boolean isCredentialsNonExpired() { return this.credentialsNonExpired; } public void eraseCredentials() { this.password = null; } private static SortedSet sortAuthorities(Collection extends GrantedAuthority> authorities) { Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection"); SortedSet sortedAuthorities = new TreeSet(new AuthorityComparator()); Iterator var2 = authorities.iterator(); while(var2.hasNext()) { GrantedAuthority grantedAuthority = (GrantedAuthority)var2.next(); Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements"); sortedAuthorities.add(grantedAuthority); } return sortedAuthorities; } public boolean equals(Object obj) { return obj instanceof User ? this.username.equals(((User)obj).username) : false; } public int hashCode() { return this.username.hashCode(); } public String toString() { StringBuilder sb = new StringBuilder(); sb.append(this.getClass().getName()).append(" ["); sb.append("Username=").append(this.username).append(", "); sb.append("Password=[PROTECTED], "); sb.append("Enabled=").append(this.enabled).append(", "); sb.append("AccountNonExpired=").append(this.accountNonExpired).append(", "); sb.append("credentialsNonExpired=").append(this.credentialsNonExpired).append(", "); sb.append("AccountNonLocked=").append(this.accountNonLocked).append(", "); sb.append("Granted Authorities=").append(this.authorities).append("]"); return sb.toString(); } public static UserBuilder withUsername(String username) { return builder().username(username); } public static UserBuilder builder() { return new UserBuilder(); } /** @deprecated */ @Deprecated public static UserBuilder withDefaultPasswordEncoder() { logger.warn("User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications."); PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); UserBuilder var10000 = builder(); Objects.requireNonNull(encoder); return var10000.passwordEncoder(encoder::encode); } public static UserBuilder withUserDetails(UserDetails userDetails) { return withUsername(userDetails.getUsername()).password(userDetails.getPassword()).accountExpired(!userDetails.isAccountNonExpired()).accountLocked(!userDetails.isAccountNonLocked()).authorities(userDetails.getAuthorities()).credentialsExpired(!userDetails.isCredentialsNonExpired()).disabled(!userDetails.isEnabled()); } public static final class UserBuilder { private String username; private String password; private List authorities; private boolean accountExpired; private boolean accountLocked; private boolean credentialsExpired; private boolean disabled; private Function passwordEncoder; private UserBuilder() { this.passwordEncoder = (password) -> { return password; }; } public UserBuilder username(String username) { Assert.notNull(username, "username cannot be null"); this.username = username; return this; } public UserBuilder password(String password) { Assert.notNull(password, "password cannot be null"); this.password = password; return this; } public UserBuilder passwordEncoder(Function encoder) { Assert.notNull(encoder, "encoder cannot be null"); this.passwordEncoder = encoder; return this; } public UserBuilder roles(String... roles) { List authorities = new ArrayList(roles.length); String[] var3 = roles; int var4 = roles.length; for(int var5 = 0; var5 < var4; ++var5) { String role = var3[var5]; Assert.isTrue(!role.startsWith("ROLE_"), () -> { return role + " cannot start with ROLE_ (it is automatically added)"; }); authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); } return this.authorities((Collection)authorities); } public UserBuilder authorities(GrantedAuthority... authorities) { return this.authorities((Collection)Arrays.asList(authorities)); } public UserBuilder authorities(Collection extends GrantedAuthority> authorities) { this.authorities = new ArrayList(authorities); return this; } public UserBuilder authorities(String... authorities) { return this.authorities((Collection)AuthorityUtils.createAuthorityList(authorities)); } public UserBuilder accountExpired(boolean accountExpired) { this.accountExpired = accountExpired; return this; } public UserBuilder accountLocked(boolean accountLocked) { this.accountLocked = accountLocked; return this; } public UserBuilder credentialsExpired(boolean credentialsExpired) { this.credentialsExpired = credentialsExpired; return this; } public UserBuilder disabled(boolean disabled) { this.disabled = disabled; return this; } public UserDetails build() { String encodedPassword = (String)this.passwordEncoder.apply(this.password); return new User(this.username, encodedPassword, !this.disabled, !this.accountExpired, !this.credentialsExpired, !this.accountLocked, this.authorities); } } private static class AuthorityComparator implements Comparator , Serializable { private static final long serialVersionUID = 560L; private AuthorityComparator() { } public int compare(GrantedAuthority g1, GrantedAuthority g2) { if (g2.getAuthority() == null) { return -1; } else { return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority()); } } } }
主要关注User的几个成员变量:
private String password; private final String username; private final Setauthorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled;
举例:
@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()); } }
配套视频:25.自定义数据源之库表设计(一)_哔哩哔哩_bilibili
26.自定义数据源之实现(二)_哔哩哔哩_bilibili
设计表结构
-- 用户表 共有多个用户 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; -- 角色表 共有多个角色,其中,_zh表示中文 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;
Navicat新建MySQL数据库security,字符集utf8mb4:
执行SQL语句:
插入测试数据
-- 插入用户数据 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; -- 插入角色数据 授权时ROLE_进行处理,RBAC 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;
security数据库上,右键新建查询,插入数据:
插入后查看表中是否包含数据:
接下来进行SpringBoot整合mybatis
项目的pom.xml中引入依赖
com.alibaba druid 1.2.7 mysql mysql-connector-java 5.1.38 org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0
application.properties进行数据源、mybatis、日志配置
# 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配置mapper文件的位置和别名设置 # 注意mapper目录(包)新建时必须使用"/",而不是. mybatis.mapper-locations=classpath:com/study/mapper/*.xml mybatis.type-aliases-package=com.study.entity # log:为了显示mybatis运行SQL语句 logging.level.com.study=debug
创建 entity实体类包
创建User 对象
package com.study.entity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.*; /** * @ClassName User * @Description 用户类,对应表user,为了与UserDetails中的User对应,此处进行实现 * @Author Jiangnan Cui * @Date 2022/8/7 11:43 * @Version 1.0 */ 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 Listroles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息 //返回权限信息 @Override public Collection extends GrantedAuthority> getAuthorities() { Set authorities = new HashSet<>(); roles.forEach(role -> { SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName()); authorities.add(simpleGrantedAuthority); }); return authorities; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public boolean isAccountNonExpired() { return accountNonLocked; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public Boolean getAccountNonExpired() { return accountNonExpired; } public void setAccountNonExpired(Boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; } public Boolean getAccountNonLocked() { return accountNonLocked; } public void setAccountNonLocked(Boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } public Boolean getCredentialsNonExpired() { return credentialsNonExpired; } public void setCredentialsNonExpired(Boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; } public List getRoles() { return roles; } public void setRoles(List roles) { this.roles = roles; } }
创建 Role 对象
package com.study.entity; /** * @ClassName Role * @Description 角色类,对应表role * @Author Jiangnan Cui * @Date 2022/8/7 11:44 * @Version 1.0 */ public class Role { private Integer id; private String name; private String nameZh; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNameZh() { return nameZh; } public void setNameZh(String nameZh) { this.nameZh = nameZh; } }
创建 UserDao 接口
package com.study.dao; import com.study.entity.Role; import com.study.entity.User; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * @ClassName UserDao * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/7 12:09 * @Version 1.0 */ @Mapper public interface UserDao { /** * 根据用户名查找用户 * @param username * @return */ User loadUserByUsername(String username); /** * 根据用户id查询一个角色,注意一个用户可能不止一种角色 * @param uid * @return */ ListgetRolesByUid(Integer uid); }
创建 UserMapper 实现
创建 UserDetailService 实例
package com.study.service; import com.study.dao.UserDao; import com.study.entity.Role; import com.study.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; import java.util.List; /** * @ClassName MyUserDetailsService * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/7 11:26 * @Version 1.0 */ @Service public class MyUserDetailsService implements UserDetailsService { private UserDao userDao; @Autowired public MyUserDetailsService(UserDao userDao) { this.userDao = userDao; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1.查询用户 User user = userDao.loadUserByUsername(username); if (ObjectUtils.isEmpty(user)) throw new RuntimeException("用户不存在"); //2.查询权限信息 Listroles = userDao.getRolesByUid(user.getId()); user.setRoles(roles); 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.. } }
启动测试:访问http://localhost:8080/login.html,登录数据库中的用户可正常访问,显示登录成功,用户不存在时抛出异常!
配套视频:27.传统web开发认证总结案例_哔哩哔哩_bilibili
新建module:spring-security-03web,引入Spring Web。
org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-security org.thymeleaf.extras thymeleaf-extras-springsecurity5 3.0.4.RELEASE com.alibaba druid 1.2.7 mysql mysql-connector-java 5.1.38 org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0
# 配置Thymeleaf spring.thymeleaf.cache=false spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.mode=HTML # 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配置mapper文件的位置和别名设置 # 注意mapper目录(包)新建时必须使用"/",而不是. mybatis.mapper-locations=classpath:com/study/mapper/*.xml mybatis.type-aliases-package=com.study.entity # log:为了显示mybatis运行SQL语句 logging.level.com.study=debug
User
package com.study.entity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.*; /** * @ClassName User * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/7 15:16 * @Version 1.0 */ 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 Listroles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息 //返回权限信息 @Override public Collection extends GrantedAuthority> getAuthorities() { Set authorities = new HashSet<>(); roles.forEach(role -> { SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName()); authorities.add(simpleGrantedAuthority); }); return authorities; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public boolean isAccountNonExpired() { return accountNonLocked; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public Boolean getAccountNonExpired() { return accountNonExpired; } public void setAccountNonExpired(Boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; } public Boolean getAccountNonLocked() { return accountNonLocked; } public void setAccountNonLocked(Boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } public Boolean getCredentialsNonExpired() { return credentialsNonExpired; } public void setCredentialsNonExpired(Boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; } public List getRoles() { return roles; } public void setRoles(List roles) { this.roles = roles; } }
Role
package com.study.entity; /** * @ClassName Role * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/7 15:17 * @Version 1.0 */ public class Role { private Integer id; private String name; private String nameZh; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNameZh() { return nameZh; } public void setNameZh(String nameZh) { this.nameZh = nameZh; } }
package com.study.dao; import com.study.entity.Role; import com.study.entity.User; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * @ClassName UserDao * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/7 15:19 * @Version 1.0 */ @Mapper public interface UserDao { /** * 根据用户名查找用户 * * @param username * @return */ User loadUserByUsername(String username); /** * 根据用户id查询一个角色,注意一个用户可能不止一种角色 * * @param uid * @return */ ListgetRolesByUid(Integer uid); }
package com.study.service; import com.study.dao.UserDao; import com.study.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; /** * @ClassName MyUserDetailsService * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/7 15:15 * @Version 1.0 */ @Service public class MyUserDetailsService implements UserDetailsService { private final UserDao userDao; @Autowired public MyUserDetailsService(UserDao userDao) { this.userDao = userDao; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.loadUserByUsername(username); if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户不存在"); user.setRoles(userDao.getRolesByUid(user.getId())); return user; } }
login.html
登录页面 用户登录
index.html
系统主页 欢迎,进入我的系统!
用户详细信息如下:
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @ClassName MvcConfigurer * @Description 对SpringMVC进行自定义配置 * @Author Jiangnan Cui * @Date 2022/8/7 14:20 * @Version 1.0 */ @Configuration public class MvcConfigurer implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { //addViewController: forward跳转 registry.addViewController("/login.html").setViewName("login"); registry.addViewController("/index.html").setViewName("index"); } }
package com.study.config; import com.study.service.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @ClassName SecurityConfiger * @Description 自定义security相关配置 * @Author Jiangnan Cui * @Date 2022/8/7 14:35 * @Version 1.0 */ @Configuration public class SecurityConfigurer extends WebSecurityConfigurerAdapter { // @Bean // public UserDetailsService userDetailsService() { // InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(); // userDetailsManager.createUser(User.withUsername("root").password("{noop}123456").roles("admin").build()); // return userDetailsManager; // } @Autowired private MyUserDetailsService myUserDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .mvcMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") //指定自定义登录页面 .loginProcessingUrl("/doLogin") .usernameParameter("uname") .passwordParameter("pwd") .defaultSuccessUrl("/index.html", true) .failureUrl("/login.html") .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login.html") .and() .csrf().disable(); } }
(1)登录数据库中含有的用户时,登录成功后显示认证信息:
(2)登录数据库中不含有的用户时,认证后显示错误信息:
配套视频:28.前后端分离认证总结案例(一)_哔哩哔哩_bilibili
新建module:spring-security-04split,勾选Spring Web,创建项目(注意:此处SpringBoot使用的是2.6.2版本),启动服务,查看服务是否能够正常启动!
package com.study.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @ClassName TestController * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/9 16:11 * @Version 1.0 */ @RestController public class TestController { @GetMapping("/test") public String test() { System.out.println("test is ok!"); return "Test is Ok!"; } }
启动服务,浏览器访问:http://localhost:8080/test,浏览器输出:Test is Ok!",IDEA控制台输出:test is ok!,表示项目创建成功!
org.springframework.boot spring-boot-starter-security
刷新maven,重启启动服务,浏览器再次访问:http://localhost:8080/test,此时需要进行登录认证,用户名为:user,密码为引入SpringSecurity框架后IDEA控制台产生的随机密码,输入正确的用户名和密码登录成功后,浏览器输出:Test is Ok!",IDEA控制台输出:test is ok!,表示项目创建成功!
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @ClassName SecurityConfig * @Description SpringSecurity配置类 * @Author Jiangnan Cui * @Date 2022/8/9 17:36 * @Version 1.0 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated()//所有请求必须认证 .and() .formLogin();//form表单认证登录 } }
在进行前后端分离认证时,后端接收到的是前端发送过来的post形式的json数据,此时需要在json格式中获取用于登录的用户名和密码,而SpringSecurity中用于对用户名和密码进行拦截的过滤器为UsernamePasswordAuthenticationFilter,此时需要对此Filter进行覆盖,重写attemptAuthentication()方法后得到用户名和密码,从而进行认证。
package com.study.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; /** * @ClassName LoginFilter * @Description 自定义前后端分离Filter * @Author Jiangnan Cui * @Date 2022/8/9 17:42 * @Version 1.0 */ public class LoginFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { System.out.println("========================================"); //1.判断是否是 post 方式请求 if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } //2.判断是否是 json 格式请求类型 if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) { //3.从 json 数据中获取用户输入用户名和密码进行认证 {"uname":"xxx","password":"xxx"} try { MapuserInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class); String username = userInfo.get(getUsernameParameter()); String password = userInfo.get(getPasswordParameter()); System.out.println("用户名: " + username + " 密码: " + password); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } catch (IOException e) { e.printStackTrace(); } } return super.attemptAuthentication(request, response); } }
package com.study.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * @ClassName SecurityConfig * @Description SpringSecurity配置类 * @Author Jiangnan Cui * @Date 2022/8/9 17:36 * @Version 1.0 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { //自定义AuthenticationManager @Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(); inMemoryUserDetailsManager.createUser( User.withUsername("root").password("{noop}123456").roles("admin").build()); return inMemoryUserDetailsManager; } //覆盖默认的AuthenticationManager @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()); } //从工厂中暴露出来 @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } //把自定义Filter交给工厂,用以替换UsernamePasswordAuthenticationFilter @Bean public LoginFilter loginFilter() throws Exception { LoginFilter loginFilter = new LoginFilter(); loginFilter.setFilterProcessesUrl("/doLogin");//指定认证url loginFilter.setUsernameParameter("uname");//指定接收json用户名key loginFilter.setPasswordParameter("pwd");//指定接收json密码key loginFilter.setAuthenticationManager(authenticationManagerBean()); //认证成功处理 loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Mapresult = new HashMap (); result.put("msg", "登录成功");//打印登录成功信息 //result.put("status", 200);//打印状态码 此处改为setStatus result.put("用户信息", authentication.getPrincipal());//获得身份信息 result.put("authentication", authentication);//打印认证信息 response.setContentType("application/json;charset=UTF-8");//设置响应类型 response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态 String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串 response.getWriter().println(s);//打印json格式数据 } }); //认证失败处理 loginFilter.setAuthenticationFailureHandler((request, response, exception) -> { Map result = new HashMap (); result.put("msg", "登录失败:" + exception.getMessage()); //result.put("status", 500);//打印状态码 此处改为setStatus response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//设置登录失败之后的状态 String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); }); return loginFilter; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated()//所有请求必须认证 .and() .formLogin()//form表单认证时默认采用UsernamePasswordAuthenticationFilter进行拦截 // 此处使用LoginFilter进行替换 .and() .exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPoint() { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().println("请认证之后再去处理!"); } }) .and() .logout() //.logoutUrl("/logout") .logoutRequestMatcher(new OrRequestMatcher( new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()), new AntPathRequestMatcher("/logout", HttpMethod.GET.name()) )) .logoutSuccessHandler((request, response, authentication) -> { Map result = new HashMap (); result.put("msg", "注销成功");//打印登录成功信息 //result.put("status", 200);//打印状态码 此处改为setStatus result.put("用户信息", authentication.getPrincipal());//获得身份信息 result.put("authentication", authentication);//打印认证信息 response.setContentType("application/json;charset=UTF-8");//设置响应类型 response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态 String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串 response.getWriter().println(s);//打印json格式数据 }) .and() .csrf().disable(); /** * addFilterAt:用某个filter替换掉过滤器链中的某个filter * addFilterBefore():将某个过滤器放在过滤器链中某个filter之前 * addFilterAfter():将某个过滤器放在过滤器链中某个filter之后 */ http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class); } }
启动服务,打开Postman进行测试:
(1)GET http://localhost:8080/test
(2)POST http://localhost:8080/doLogin
(3)GET或DELETE http://localhost:8080/logout
配套视频:29.前后端分离认证总结案例(二)_哔哩哔哩_bilibili
pom.xml引入依赖
com.alibaba druid 1.2.7 mysql mysql-connector-java 5.1.38 org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0
刷新maven!
application.properties添加配置
# 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配置mapper文件的位置和别名设置 # 注意mapper目录(包)新建时必须使用"/",而不是. mybatis.mapper-locations=classpath:com/study/mapper/*.xml mybatis.type-aliases-package=com.study.entity # log:为了显示mybatis运行SQL语句 logging.level.com.study=debug
创建实体类entity
User
package com.study.entity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.*; /** * @ClassName User * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/10 0:25 * @Version 1.0 */ 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 Listroles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息 //返回权限信息 @Override public Collection extends GrantedAuthority> getAuthorities() { Set authorities = new HashSet<>(); roles.forEach(role -> { SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName()); authorities.add(simpleGrantedAuthority); }); return authorities; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public boolean isAccountNonExpired() { return accountNonLocked; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public Boolean getAccountNonExpired() { return accountNonExpired; } public void setAccountNonExpired(Boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; } public Boolean getAccountNonLocked() { return accountNonLocked; } public void setAccountNonLocked(Boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } public Boolean getCredentialsNonExpired() { return credentialsNonExpired; } public void setCredentialsNonExpired(Boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; } public List getRoles() { return roles; } public void setRoles(List roles) { this.roles = roles; } }
Role
package com.study.entity; /** * @ClassName Role * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/10 0:25 * @Version 1.0 */ public class Role { private Integer id; private String name; private String nameZh; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNameZh() { return nameZh; } public void setNameZh(String nameZh) { this.nameZh = nameZh; } }
创建UserDao
package com.study.dao; import com.study.entity.Role; import com.study.entity.User; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * @ClassName UserDao * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/10 0:27 * @Version 1.0 */ @Mapper public interface UserDao { /** * 根据用户名查找用户 * * @param username * @return */ User loadUserByUsername(String username); /** * 根据用户id查询一个角色,注意一个用户可能不止一种角色 * * @param uid * @return */ ListgetRolesByUid(Integer uid); }
创建UserDaoMapper.xml
创建MyUserDetailsService
package com.study.service; import com.study.dao.UserDao; import com.study.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; /** * @ClassName MyUserDetailsService * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/10 0:26 * @Version 1.0 */ @Service public class MyUserDetailsService implements UserDetailsService { private final UserDao userDao; @Autowired public MyUserDetailsService(UserDao userDao) { this.userDao = userDao; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.loadUserByUsername(username); if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户不存在"); user.setRoles(userDao.getRolesByUid(user.getId())); return user; } }
SecurityConfig修改数据源
package com.study.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.study.service.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * @ClassName SecurityConfig * @Description SpringSecurity配置类 * @Author Jiangnan Cui * @Date 2022/8/9 17:36 * @Version 1.0 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { //自定义AuthenticationManager // @Bean // public UserDetailsService userDetailsService() { // InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(); // inMemoryUserDetailsManager.createUser( // User.withUsername("root").password("{noop}123456").roles("admin").build()); // return inMemoryUserDetailsManager; // } //引入数据源 private MyUserDetailsService myUserDetailsService; @Autowired public SecurityConfig(MyUserDetailsService myUserDetailsService) { this.myUserDetailsService = myUserDetailsService; } //覆盖默认的AuthenticationManager @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //auth.userDetailsService(userDetailsService()); auth.userDetailsService(myUserDetailsService); } //从工厂中暴露出来 @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } //把自定义Filter交给工厂,用以替换UsernamePasswordAuthenticationFilter @Bean public LoginFilter loginFilter() throws Exception { LoginFilter loginFilter = new LoginFilter(); loginFilter.setFilterProcessesUrl("/doLogin");//指定认证url loginFilter.setUsernameParameter("uname");//指定接收json用户名key loginFilter.setPasswordParameter("pwd");//指定接收json密码key loginFilter.setAuthenticationManager(authenticationManagerBean()); //认证成功处理 loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Mapresult = new HashMap (); result.put("msg", "登录成功");//打印登录成功信息 //result.put("status", 200);//打印状态码 此处改为setStatus result.put("用户信息", authentication.getPrincipal());//获得身份信息 result.put("authentication", authentication);//打印认证信息 response.setContentType("application/json;charset=UTF-8");//设置响应类型 response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态 String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串 response.getWriter().println(s);//打印json格式数据 } }); //认证失败处理 loginFilter.setAuthenticationFailureHandler((request, response, exception) -> { Map result = new HashMap (); result.put("msg", "登录失败:" + exception.getMessage()); //result.put("status", 500);//打印状态码 此处改为setStatus response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//设置登录失败之后的状态 String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); }); return loginFilter; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated()//所有请求必须认证 .and() .formLogin()//form表单认证时默认采用UsernamePasswordAuthenticationFilter进行拦截 // 此处使用LoginFilter进行替换 .and() .exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPoint() { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().println("请认证之后再去处理!"); } }) .and() .logout() //.logoutUrl("/logout") .logoutRequestMatcher(new OrRequestMatcher( new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()), new AntPathRequestMatcher("/logout", HttpMethod.GET.name()) )) .logoutSuccessHandler((request, response, authentication) -> { Map result = new HashMap (); result.put("msg", "注销成功");//打印登录成功信息 //result.put("status", 200);//打印状态码 此处改为setStatus result.put("用户信息", authentication.getPrincipal());//获得身份信息 result.put("authentication", authentication);//打印认证信息 response.setContentType("application/json;charset=UTF-8");//设置响应类型 response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态 String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串 response.getWriter().println(s);//打印json格式数据 }) .and() .csrf().disable(); /** * addFilterAt:用某个filter替换掉过滤器链中的某个filter * addFilterBefore():将某个过滤器放在过滤器链中某个filter之前 * addFilterAfter():将某个过滤器放在过滤器链中某个filter之后 */ http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class); } }
启动服务,打开Postman进行测试:
(1)GET http://localhost:8080/test 登录失败
(2)POST http://localhost:8080/doLogin
uname:root,pwd:123
uname:blr,pwd:123
uname:admin,pwd:123
均可成功登录
(3)GET或DELETE http://localhost:8080/logout 成功注销
配套视频:30.传统 web 开发之添加验证码(一)_哔哩哔哩_bilibili
配套视频:31.传统 web 开发之添加验证码(二)_哔哩哔哩_bilibili
新建module:spring-security-05,勾选Spring Web,创建项目(注意:此处SpringBoot使用的是2.6.2版本),启动服务,查看服务是否能够正常启动!
org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-security
刷新Maven!
spring.thymeleaf.cache=false spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html
login.html
登录页面 用户登录
index.html
系统主页 欢迎来到小崔的主页!
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @ClassName MvcConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 12:14 * @Version 1.0 */ @Configuration public class MvcConfigurer implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index.html").setViewName("index"); registry.addViewController("/login.html").setViewName("login"); } }
package com.study.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; /** * @ClassName SecurityConfigurer * @Description 配置Security * @Author Jiangnan Cui * @Date 2022/8/11 12:26 * @Version 1.0 */ @Configuration public class SecurityConfigurer extends WebSecurityConfigurerAdapter { public UserDetailsService userDetailsService() { InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(); inMemoryUserDetailsManager.createUser( User.withUsername("root"). password("{noop}123456"). roles("admin"). build()); return inMemoryUserDetailsManager; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .mvcMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .loginProcessingUrl("/doLogin") .usernameParameter("uname") .passwordParameter("pwd") .defaultSuccessUrl("/index.html", true) .failureUrl("/login.html") .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login.html") .and() .csrf().disable(); } }
启动服务,浏览器访问:http://localhost:8080/index.html,此时需要进行登录认证,用户名为root,密码为123456,输入后正常访问index.html,表示测试成功!注销登录访问路径:http://localhost:8080/login.html。
pom.xml引入依赖kaptcha
com.github.penggle kaptcha 2.3.2
添加验证码配置
package com.study.config; import com.google.code.kaptcha.Producer; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Properties; /** * @ClassName KaptchaConfigurer * @Description 生成验证码 * @Author Jiangnan Cui * @Date 2022/8/11 16:16 * @Version 1.0 */ @Configuration public class KaptchaConfigurer { @Bean public Producer kaptcha() { Properties properties = new Properties(); //1.验证码宽度 properties.setProperty("kaptcha.image.width", "150"); //2.验证码高度 properties.setProperty("kaptcha.image.height", "50"); //3.验证码字符串 properties.setProperty("kaptcha.textproducer.char.string", "0123456789"); //4.验证码长度 properties.setProperty("kaptcha.textproducer.char.length", "4"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
登录页面 用户登录
并在SecurityConfigurer对 http://localhost:8080/vc.jpg 请求进行放行。
package com.study.controller; import com.google.code.kaptcha.Producer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.awt.image.BufferedImage; import java.io.IOException; /** * @ClassName VerifyCodeController * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 16:20 * @Version 1.0 */ @Controller public class VerifyCodeController { private final Producer producer; @Autowired public VerifyCodeController(Producer producer) { this.producer = producer; } @RequestMapping("/vc.jpg") public void verityCode(HttpServletResponse response, HttpSession session) throws IOException { //1.生成验证码 String verifyCode = producer.createText(); //2.保存到session中 session.setAttribute("kaptcha", verifyCode); //3.生成图片 BufferedImage bi = producer.createImage(verifyCode); //4.设置响应类型,并响应成图片 response.setContentType("image/png"); ServletOutputStream os = response.getOutputStream(); ImageIO.write(bi, "jpg", os); } }
启动服务,访问 http://localhost:8080/vc.jpg 可得到随机生成的四位验证码图片:
访问 http://localhost:8080/vc.jpg 可发现登录页出现验证码:
package com.study.filter; import com.study.exception.KaptchaNotMatchException; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.util.ObjectUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @ClassName KaptchaFilter * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 17:29 * @Version 1.0 */ 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; } @Override public 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 verifyCode = request.getParameter(getKaptcha()); //与session中的验证码进行比较 String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha"); if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equalsIgnoreCase(sessionVerifyCode)) { return super.attemptAuthentication(request, response); } //不满足条件时直接抛出异常 throw new KaptchaNotMatchException("验证码不匹配"); } }
package com.study.exception; import org.springframework.security.core.AuthenticationException; /** * @ClassName KaptchaNotMatchException * @Description 自定义验证码认证异常 * @Author Jiangnan Cui * @Date 2022/8/11 17:39 * @Version 1.0 */ public class KaptchaNotMatchException extends AuthenticationException { public KaptchaNotMatchException(String msg) { super(msg); } public KaptchaNotMatchException(String msg, Throwable cause) { super(msg, cause); } }
package com.study.config; import com.study.filter.KaptchaFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * @ClassName SecurityConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 12:26 * @Version 1.0 */ @Configuration public class SecurityConfigurer extends WebSecurityConfigurerAdapter { public UserDetailsService userDetailsService() { InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(); inMemoryUserDetailsManager.createUser( User.withUsername("root"). password("{noop}123456"). roles("admin"). build()); return inMemoryUserDetailsManager; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()); } @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public KaptchaFilter kaptchaFilter() throws Exception { KaptchaFilter kaptchaFilter = new KaptchaFilter(); //指定接收验证码请求参数名 kaptchaFilter.setKaptcha("kaptcha"); //指定认证管理器 kaptchaFilter.setAuthenticationManager(authenticationManagerBean()); //指定认证成功处理 kaptchaFilter.setAuthenticationSuccessHandler((res, resp, auth) -> { resp.sendRedirect("/index.html"); }); //指定认证失败处理 kaptchaFilter.setAuthenticationFailureHandler((res, resp, auth) -> { resp.sendRedirect("/login.html"); }); return kaptchaFilter; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .mvcMatchers("/login.html").permitAll() .mvcMatchers("/vc.jpg").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .loginProcessingUrl("/doLogin") .usernameParameter("uname") .passwordParameter("pwd") // .defaultSuccessUrl("/index.html", true) // .failureUrl("/login.html") .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login.html") .and() .csrf().disable(); http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class); } }
启动服务,访问 http://localhost:8080/login.html ,输入正确的用户名、密码、验证码后可正常登录,表示测试成功!
配套视频:32.前后端分离开发之添加验证码_哔哩哔哩_bilibili
新建module:spring-security-06,勾选Spring Web、Spring Security,创建项目(注意:此处SpringBoot使用的是2.6.2版本),启动服务,查看服务是否能够正常启动!
com.github.penggle kaptcha 2.3.2 com.alibaba druid 1.2.7 mysql mysql-connector-java 5.1.38 org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0
刷新Maven!
# datasource:类型、驱动名、url、用户名、密码 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配置mapper文件的位置和别名设置 # 注意mapper目录(包)新建时必须使用"/",而不是. mybatis.mapper-locations=classpath:com/study/mapper/*.xml mybatis.type-aliases-package=com.study.entity # log:为了显示mybatis运行SQL语句 logging.level.com.study=debug
User
package com.study.entity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.*; /** * @ClassName User * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 22:58 * @Version 1.0 */ 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 Listroles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息 //返回权限信息 @Override public Collection extends GrantedAuthority> getAuthorities() { Set authorities = new HashSet<>(); roles.forEach(role -> { SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName()); authorities.add(simpleGrantedAuthority); }); return authorities; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public boolean isAccountNonExpired() { return accountNonLocked; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public Boolean getAccountNonExpired() { return accountNonExpired; } public void setAccountNonExpired(Boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; } public Boolean getAccountNonLocked() { return accountNonLocked; } public void setAccountNonLocked(Boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } public Boolean getCredentialsNonExpired() { return credentialsNonExpired; } public void setCredentialsNonExpired(Boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; } public List getRoles() { return roles; } public void setRoles(List roles) { this.roles = roles; } }
Role
package com.study.entity; /** * @ClassName Role * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 22:59 * @Version 1.0 */ public class Role { private Integer id; private String name; private String nameZh; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNameZh() { return nameZh; } public void setNameZh(String nameZh) { this.nameZh = nameZh; } }
package com.study.dao; import com.study.entity.Role; import com.study.entity.User; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * @ClassName UserDao * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 22:59 * @Version 1.0 */ @Mapper public interface UserDao { User loadUserByUsername(String username); /** * 根据用户id查询一个角色,注意一个用户可能不止一种角色 * * @param uid * @return */ ListgetRolesByUid(Integer uid); }
package com.study.service; import com.study.dao.UserDao; import com.study.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; /** * @ClassName MyUserDetailsService * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 23:02 * @Version 1.0 */ @Service public class MyUserDetailsService implements UserDetailsService { private final UserDao userDao; @Autowired public MyUserDetailsService(UserDao userDao) { this.userDao = userDao; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.loadUserByUsername(username); if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户不存在"); user.setRoles(userDao.getRolesByUid(user.getId())); return user; } }
KaptchaConfigurer 配置
package com.study.config; import com.google.code.kaptcha.Producer; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Properties; /** * @ClassName KaptchaConfigurer * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 23:04 * @Version 1.0 */ @Configuration public class KaptchaConfigurer { @Bean public Producer kaptcha() { Properties properties = new Properties(); //1.验证码宽度 properties.setProperty("kaptcha.image.width", "150"); //2.验证码高度 properties.setProperty("kaptcha.image.height", "50"); //3.验证码字符串 properties.setProperty("kaptcha.textproducer.char.string", "0123456789"); //4.验证码长度 properties.setProperty("kaptcha.textproducer.char.length", "4"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
VerityCodeController 控制器
package com.study.controller; import com.google.code.kaptcha.Producer; import org.apache.tomcat.util.codec.binary.Base64; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.FastByteArrayOutputStream; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.imageio.ImageIO; import javax.servlet.http.HttpSession; import java.awt.image.BufferedImage; import java.io.IOException; /** * @ClassName VerifyCodeController * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 23:06 * @Version 1.0 */ @RestController //一定是RestController而非Controller public class VerifyCodeController { private final Producer producer; @Autowired public VerifyCodeController(Producer producer) { this.producer = producer; } @GetMapping("/vc.jpg") public String getVerifyCode(HttpSession session) throws IOException { //1.生成验证码 String verifyCode = producer.createText(); //2.存入Session或Redis session.setAttribute("kaptcha", verifyCode); //3.生成图片 BufferedImage bi = producer.createImage(verifyCode); //图片转化为字节 FastByteArrayOutputStream fbaos = new FastByteArrayOutputStream(); ImageIO.write(bi, "jpg", fbaos); //4.返回base64 return Base64.encodeBase64String(fbaos.toByteArray()); } }
LoginKaptchaFilter 过滤器
package com.study.filter; import com.fasterxml.jackson.databind.ObjectMapper; import com.study.exception.KaptchaNotMatchException; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.util.ObjectUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; /** * @ClassName LoginKaptchaFilter * @Description 自定义Filter * @Author Jiangnan Cui * @Date 2022/8/11 23:17 * @Version 1.0 */ 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; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //1.判断是否是POST请求 if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } try { //2.获取请求数据 MapuserInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class); String kaptcha = userInfo.get(getKaptchaParameter());//用来获取数据中验证码 String username = userInfo.get(getUsernameParameter());//用来接收用户名 String password = userInfo.get(getPasswordParameter());//用来接收密码 //3.获取session中验证码 String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha"); if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionVerifyCode) && kaptcha.equalsIgnoreCase(sessionVerifyCode)) { //4.获取用户名和密码认证 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } catch (IOException e) { e.printStackTrace(); } throw new KaptchaNotMatchException("验证码不匹配"); } }
定义验证码异常类
public class KaptchaNotMatchException extends AuthenticationException { public KaptchaNotMatchException(String msg) { super(msg); } public KaptchaNotMatchException(String msg, Throwable cause) { super(msg, cause); } }
package com.study.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.study.filter.LoginKaptchaFilter; import com.study.service.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import java.util.HashMap; import java.util.Map; /** * @ClassName SecurityConfig * @Description TODO * @Author Jiangnan Cui * @Date 2022/8/11 23:06 * @Version 1.0 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { //自定义内存数据源 private MyUserDetailsService myUserDetailsService; @Autowired public SecurityConfig(MyUserDetailsService myUserDetailsService) { this.myUserDetailsService = myUserDetailsService; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } //配置 @Bean public LoginKaptchaFilter loginKaptchaFilter() throws Exception { LoginKaptchaFilter loginKaptchaFilter = new LoginKaptchaFilter(); //1.认证 url loginKaptchaFilter.setFilterProcessesUrl("/doLogin"); //2.认证 接收参数 loginKaptchaFilter.setUsernameParameter("uname"); loginKaptchaFilter.setPasswordParameter("passwd"); loginKaptchaFilter.setKaptchaParameter("kaptcha"); //3.指定认证管理器 loginKaptchaFilter.setAuthenticationManager(authenticationManagerBean()); //4.指定成功时处理 loginKaptchaFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> { Mapresult = new HashMap (); 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 result = new HashMap (); 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; } @Override protected 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); } }
启动服务,打开postman,测试过程如下:
(1)生成验证码:http://localhost:8080/vc.jpg
破解base64格式的验证码:图片Base64编码互转工具-在线工具,复制上面的数据到输入框内,前面加上前缀:data:image/png;base64,得到对应的4位验证码数字。
(2)测试登录:http://localhost:8080/doLogin,测试数据均为数据库已有数据