開發工具:ide、數據庫:mysql5.七、springboot版本:2.3.7javascript
我的對Spring Security的執行過程大體理解(僅供參考)css
使用Spring Security很簡單,只要在pom.xml文件中,引入spring security的依賴就能夠了html
pom配置:前端
org.springframework.bootspring-boot-starter-security
這個時候咱們不在配置文件中作任何配置,隨便寫一個Controller java
@RestControllerpublic class TestController { @GetMapping("/hello") public String request() { return "hello"; } }
啓動項目,咱們會發現有這麼一段日誌mysql
此時表示Security生效,默認對項目進行了保護,咱們訪問該Controller中的接口(http://localhost:8080/securitydemo/hello),會見到以下登陸界面(此界面爲security框架自帶的默認登陸界面,後期不用能夠換成自定義登陸界面)jquery
這裏面的用戶名和密碼是什麼呢?此時咱們須要輸入用戶名:user,密碼則爲以前日誌中的"19262f35-9ded-49c2-a8f6-5431536cc50c",輸入以後,咱們能夠看到此時能夠正常訪問該接口web
在老版本的Springboot中(好比說Springboot 1.x版本中),能夠經過以下方式來關閉Spring Security的生效,可是如今Springboot 2中已經再也不支持ajax
security: basic: enabled: false
springboot2.x後能夠在啓動類中設置spring
一、配置基於內存的角色受權和認證信息
1.1目錄
1.2 WebSecurityConfg配置類
Spring Security的核心配置類是 WebSecurityConfigurerAdapter抽象類,這是權限管理啓動的入口,這裏咱們自定義一個實現類去實現它。
/** * @Author qt * @Date 2021/3/25 * @Description SpringSecurity安全框架配置 */@Configuration @EnableWebSecurity//開啓Spring Security的功能//prePostEnabled屬性決定Spring Security在接口前註解是否可用@PreAuthorize,@PostAuthorize等註解,設置爲true,會攔截加了這些註解的接口@EnableGlobalMethodSecurity(prePostEnabled=true)public class WebSecurityConfg extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { /** * 基於內存的方式,建立兩個用戶admin/123456,user/123456 * */ auth.inMemoryAuthentication() .withUser("admin")//用戶名 .password(passwordEncoder().encode("123456"))//密碼 .roles("ADMIN");//角色 auth.inMemoryAuthentication() .withUser("user")//用戶名 .password(passwordEncoder().encode("123456"))//密碼 .roles("USER");//角色 } /** * 指定加密方式 */ @Bean public PasswordEncoder passwordEncoder(){ // 使用BCrypt加密密碼 return new BCryptPasswordEncoder(); } }
1.3 MainController控制器接口
/** * @Author qt * @Date 2021/3/25 * @Description 主控制器 */@RestControllerpublic class MainController { @GetMapping("/hello") public String printStr(){ System.out.println("hello success"); return "Hello success!"; } }
這樣從新運行後咱們就能夠經過admin/12345六、user/123456兩個用戶登陸了。
固然,你也能夠基於配置文件建立用戶帳號,在pom.xml中添加:
二、配置基於數據庫的認證信息和角色受權
2.1 目錄
2.2 CustomUserDetailsService實現類
UserDetailsService是須要實現的登陸用戶查詢的service接口,實現loadUserByUsername()方法,這裏咱們自定義CustomUserDetailsService實現類去實現UserDetailsService接口
/** * @Author qt * @Date 2021/3/25 * @Description */@Componentpublic class CustomUserDetailsService implements UserDetailsService { @Resource private UserInfoService userInfoService; @Resource private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { /** * 1/經過userName 獲取到userInfo信息 * 2/經過User(UserDetails)返回details。 */ //經過userName獲取用戶信息 UserInfo userInfo = userInfoService.getUserInfoByUsername(userName); if(userInfo == null) { throw new UsernameNotFoundException("not found"); } //定義權限列表. Listauthorities = new ArrayList<>(); // 用戶能夠訪問的資源名稱(或者說用戶所擁有的權限) 注意:必須"ROLE_"開頭 authorities.add(new SimpleGrantedAuthority("ROLE_"+ userInfo.getRole())); User userDetails = new User(userInfo.getUserName(),passwordEncoder.encode(userInfo.getPassword()),authorities); return userDetails; } }
WebSecurityConfg配置類:
/** * @Author qt * @Date 2021/3/25 * @Description SpringSecurity安全框架配置 */@Configuration @EnableWebSecurity//開啓Spring Security的功能//prePostEnabled屬性決定Spring Security在接口前註解是否可用@PreAuthorize,@PostAuthorize等註解,設置爲true,會攔截加了這些註解的接口@EnableGlobalMethodSecurity(prePostEnabled=true)public class WebSecurityConfg extends WebSecurityConfigurerAdapter { /** * 指定加密方式 */ @Bean public PasswordEncoder passwordEncoder(){ // 使用BCrypt加密密碼 return new BCryptPasswordEncoder(); } }
對於經過userName獲取用戶信息的服務層,持久層和數據庫語句就不介紹了,這裏使用的是SSM框架,使用mybaits。
2.3 數據庫設計
角色表 roles
用戶表 user
用戶角色關係表 roles_user
三、自定義表單認證登陸
3.1 目錄
3.2 WebSecurityConfg核心配置類
/** * @Author qt * @Date 2021/3/25 * @Description spring-security權限管理的核心配置 */@Configuration @EnableWebSecurity//開啓Spring Security的功能//prePostEnabled屬性決定Spring Security在接口前註解是否可用@PreAuthorize,@PostAuthorize等註解,設置爲true,會攔截加了這些註解的接口@EnableGlobalMethodSecurity(prePostEnabled=true)public class WebSecurityConfg extends WebSecurityConfigurerAdapter { @Resource private AuthenticationSuccessHandler loginSuccessHandler; //認證成功結果處理器 @Resource private AuthenticationFailureHandler loginFailureHandler; //認證失敗結果處理器 //http請求攔截配置 @Override protected void configure(HttpSecurity http) throws Exception { http.headers().frameOptions().disable();//開啓運行iframe嵌套頁面 http//一、配置權限認證 .authorizeRequests() //配置不攔截路由 .antMatchers("/500").permitAll() .antMatchers("/403").permitAll() .antMatchers("/404").permitAll() .antMatchers("/login").permitAll() .anyRequest() //任何其它請求 .authenticated() //都須要身份認證 .and() //二、登陸配置表單認證方式 .formLogin() .loginPage("/login")//自定義登陸頁面的url .usernameParameter("username")//設置登陸帳號參數,與表單參數一致 .passwordParameter("password")//設置登陸密碼參數,與表單參數一致 // 告訴Spring Security在發送指定路徑時處理提交的憑證,默認狀況下,將用戶重定向回用戶來自的頁面。登陸表單form中action的地址,也就是處理認證請求的路徑, // 只要保持表單中action和HttpSecurity裏配置的loginProcessingUrl一致就能夠了,也不用本身去處理,它不會將請求傳遞給Spring MVC和您的控制器,因此咱們就不須要本身再去寫一個/user/login的控制器接口了 .loginProcessingUrl("/user/login")//配置默認登陸入口 .defaultSuccessUrl("/index")//登陸成功後默認的跳轉頁面路徑 .failureUrl("/login?error=true") .successHandler(loginSuccessHandler)//使用自定義的成功結果處理器 .failureHandler(loginFailureHandler)//使用自定義失敗的結果處理器 .and() //三、註銷 .logout() .logoutUrl("/logout") .logoutSuccessHandler(new CustomLogoutSuccessHandler()) .permitAll() .and() //四、session管理 .sessionManagement() .invalidSessionUrl("/login") //失效後跳轉到登錄頁面 //單用戶登陸,若是有一個登陸了,同一個用戶在其餘地方登陸將前一個剔除下線 //.maximumSessions(1).expiredSessionStrategy(expiredSessionStrategy()) //單用戶登陸,若是有一個登陸了,同一個用戶在其餘地方不能登陸 //.maximumSessions(1).maxSessionsPreventsLogin(true) ; .and() //五、禁用跨站csrf***防護 .csrf() .disable(); } @Override public void configure(WebSecurity web) throws Exception { //配置靜態文件不須要認證 web.ignoring().antMatchers("/static/**"); } /** * 指定加密方式 */ @Bean public PasswordEncoder passwordEncoder(){ // 使用BCrypt加密密碼 return new BCryptPasswordEncoder(); } }踩坑點1:登陸頁面接口/login和登陸驗證接口/user/login,這裏是本身以前一直搞錯的重點,這裏就用網上的圖片展現了 踩坑點2:springboot配置spring security 靜態資源不能訪問
security的配置:在類WebSecurityConfig繼承WebSecurityConfigurerAdapter,這個類是咱們在配置security的時候,對咱們請求的url及權限規則的一些認證配置。具體的不說了,這裏主要是靜態資源的問題。
在這個類中咱們會重寫一些方法,其中就有一個方法,能夠爲咱們配置一下靜態資源不須要認證。
@Override public void configure(WebSecurity web) throws Exception { //配置靜態文件不須要認證 web.ignoring().antMatchers("/static/**"); }
頁面的引用以下:
以後咱們啓動項目:看到css並無生效
這時候僅僅經過spring security配置是不夠的,咱們還須要去重寫addResourceHandlers方法去映射下靜態資源,這個方法應該很熟悉了,咱們經過springboot添加攔截器的時候就會用到這個。
寫一個類WebMvcConfig繼承WebMvcConfigurationSupport,注意spring boot2版本和1版本是不同的,spring boot1版本繼承的WebMvcConfigurerAdapter在spring boot2版本中已經提示過期了
@Configurationpublic class WebMvcConfig extends WebMvcConfigurationSupport { /** * 配置靜態資源 * @param registry */ @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); super.addResourceHandlers(registry); } }
如今從新啓動項目:css文件已經引用成功。
3.3 ErrorPageConfig 配置錯誤頁面
/** * @Author qt * @Date 2021/3/25 * @Description 配置錯誤頁面 403 404 500 適用於 SpringBoot 2.x */@Configurationpublic class ErrorPageConfig { @Bean public WebServerFactoryCustomizer webServerFactoryCustomizer() { WebServerFactoryCustomizerwebCustomizer = new WebServerFactoryCustomizer() { @Override public void customize(ConfigurableWebServerFactory factory) { ErrorPage[] errorPages = new ErrorPage[] { new ErrorPage(HttpStatus.FORBIDDEN, "/403"), new ErrorPage(HttpStatus.NOT_FOUND, "/404"), new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500"), }; factory.addErrorPages(errorPages); } }; return webCustomizer; } }
3.4 MainController 控制器
/** * @Author qt * @Date 2021/3/25 * @Description 主控制器 */@Controllerpublic class MainController { private Logger logger = LoggerFactory.getLogger(getClass()); @GetMapping("/login") public String loginPage(){ System.out.println("login page"); return "login"; } @GetMapping("/index") @PreAuthorize("hasAnyRole('USER','ADMIN')") public String index(){ System.out.println("index page"); return "index"; } @GetMapping("/admin") @PreAuthorize("hasAnyRole('ADMIN')") public String printAdmin(){ System.out.println("hello admin"); return "admin"; } @GetMapping("/user") @PreAuthorize("hasAnyRole('USER','ADMIN')") public String printUser(){ System.out.println("hello user"); return "user"; } /** * 找不到頁面 */ @GetMapping("/404") public String notFoundPage() { return "/error/404"; } /** * 未受權 */ @GetMapping("/403") public String accessError() { return "/error/403"; } /** * 服務器錯誤 */ @GetMapping("/500") public String internalError() { return "/error/500"; } }
3.5 UserInfoController 用戶控制器
/** * @Author qt * @Date 2021/3/25 * @Description */@Controller @RequestMapping("/user")public class UserInfoController { private Logger logger = LoggerFactory.getLogger(getClass()); @Resource private UserInfoService userInfoService; @GetMapping("/getUserInfo") @ResponseBody public User getUserInfo(@RequestParam String username){ return userInfoService.getUserInfoByUsername(username); } }
SMM框架的其餘部分就省略了,非這裏重點。
3.6 CustomAccessDecisionManager 自定義權限決策管理器
/** * @Author qt * @Date 2021/3/31 * @Description 自定義權限決策管理器 */@Componentpublic class CustomAccessDecisionManager implements AccessDecisionManager { /** * @Author: qt * @Description: 取當前用戶的權限與此次請求的這個url須要的權限做對比,決定是否放行 * auth 包含了當前的用戶信息,包括擁有的權限,即以前UserDetailsService登陸時候存儲的用戶對象 * object 就是FilterInvocation對象,能夠獲得request等web資源。 * configAttributes 是本次訪問須要的權限。即上一步的 MyFilterInvocationSecurityMetadataSource 中查詢覈對獲得的權限列表 **/ @Override public void decide(Authentication auth, Object o, Collectioncollection) throws AccessDeniedException, InsufficientAuthenticationException { Iteratoriterator = collection.iterator(); while (iterator.hasNext()) { if (auth == null) { throw new AccessDeniedException("當前訪問沒有權限"); } ConfigAttribute ca = iterator.next(); //當前請求須要的權限 String needRole = ca.getAttribute(); if ("ROLE_LOGIN".equals(needRole)) { if (auth instanceof AnonymousAuthenticationToken) { throw new BadCredentialsException("未登陸"); } else return; } //當前用戶所具備的權限 Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(needRole)) { return; } } } throw new AccessDeniedException("權限不足!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class aClass) { return true; } }
3.7 CustomLogoutSuccessHandler 註銷登陸處理
/** * @Author qt * @Date 2021/3/31 * @Description 註銷登陸處理 */public class CustomLogoutSuccessHandler implements LogoutSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("註銷成功!"); //這裏寫你登陸成功後的邏輯 response.setStatus(HttpStatus.OK.value()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("註銷成功!"); } }
3.8 LoginFailureHandler 登陸失敗處理
/** * @Author qt * @Date 2021/3/24 * @Description 登陸失敗處理 */@Component("loginFailureHandler")public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Resource private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info("登陸失敗"); this.saveException(request, exception); this.getRedirectStrategy().sendRedirect(request, response, "/login?error=true"); } }
3.9 LoginSuccessHandler 登陸成功處理
/** @Author qt * @Date 2021/3/24 * @Description 登陸成功處理 */@Component("loginSuccessHandler")public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Resource private ObjectMapper objectMapper; private RequestCache requestCache; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // 獲取前端傳到後端的所有參數 Enumeration enu = request.getParameterNames(); while (enu.hasMoreElements()) { String paraName = (String) enu.nextElement(); System.out.println("參數- " + paraName + " : " + request.getParameter(paraName)); } logger.info("登陸認證成功"); //這裏寫你登陸成功後的邏輯,能夠驗證其餘信息,如驗證碼等。 response.setContentType("application/json;charset=UTF-8"); JSONObject resultObj = new JSONObject(); resultObj.put("code", HttpStatus.OK.value()); resultObj.put("msg","登陸成功"); resultObj.put("authentication",objectMapper.writeValueAsString(authentication)); response.getWriter().write(resultObj.toString()); this.getRedirectStrategy().sendRedirect(request, response, "/index");//重定向 } }
3.10 login.html 登陸頁面
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head> <meta charset="UTF-8"> <title>登陸title> <link rel="stylesheet" type="text/css" th:href="@{static/layui/css/layui.css}">head><body><form method="POST" th:action="@{/user/login}"> <div> 用戶名:<input type="text" name="username" id="username"> div> <div> 密碼:<input type="password" name="password" id="password"> div> <div> <button type="submit">當即登錄button> div> 如下爲顯示認證失敗等提示信息(th:if=""必定要寫 )--> <span style="color: red;" th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}">span>form>body>html>
3.11 效果圖片
登陸失敗
登陸成功
四、自定義ajax請求認證登陸
本人比較喜歡使用ajax的登陸認證方式,這個比較靈活。
4.1 目錄
4.二、較表單登陸認證的改變
LoginFailureHandler 登陸失敗處理
/** * @Author qt * @Date 2021/3/24 * @Description 登陸失敗處理 */@Component("loginFailureHandler")public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Resource private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info("登陸失敗"); response.setContentType("application/json;charset=UTF-8"); //這裏寫你登陸失敗後的邏輯,可加驗證碼驗證等 String errorInfo = ""; if (exception instanceof BadCredentialsException || exception instanceof UsernameNotFoundException) { errorInfo = "帳戶名或者密碼輸入錯誤!"; } else if (exception instanceof LockedException) { errorInfo = "帳戶被鎖定,請聯繫管理員!"; } else if (exception instanceof CredentialsExpiredException) { errorInfo = "密碼過時,請聯繫管理員!"; } else if (exception instanceof AccountExpiredException) { errorInfo = "帳戶過時,請聯繫管理員!"; } else if (exception instanceof DisabledException) { errorInfo = "帳戶被禁用,請聯繫管理員!"; } else { errorInfo = "登陸失敗!"; } logger.info("登陸失敗緣由:" + errorInfo); //ajax請求認證方式 JSONObject resultObj = new JSONObject(); resultObj.put("code", HttpStatus.UNAUTHORIZED.value()); resultObj.put("msg",errorInfo); resultObj.put("exception",objectMapper.writeValueAsString(exception)); response.getWriter().write(resultObj.toString()); //表單認證方式 //this.saveException(request, exception); //this.getRedirectStrategy().sendRedirect(request, response, "/login?error=true"); } }
LoginSuccessHandler 登陸成功處理
/** * @Author qt * @Date 2021/3/24 * @Description 登陸成功處理 */@Component("loginSuccessHandler")public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Resource private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { response.setContentType("application/json;charset=UTF-8"); // 獲取前端傳到後端的所有參數 Enumeration enu = request.getParameterNames(); while (enu.hasMoreElements()) { String paraName = (String) enu.nextElement(); System.out.println("參數- " + paraName + " : " + request.getParameter(paraName)); } logger.info("登陸認證成功"); //這裏寫你登陸成功後的邏輯,可加驗證碼驗證等 //ajax請求認證方式 JSONObject resultObj = new JSONObject(); resultObj.put("code", HttpStatus.OK.value()); resultObj.put("msg","登陸成功"); resultObj.put("authentication",objectMapper.writeValueAsString(authentication)); response.getWriter().write(resultObj.toString()); //表單認證方式 //this.getRedirectStrategy().sendRedirect(request, response, "/index");//重定向 } }
login.html 登陸頁面
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head> <meta charset="UTF-8"> <title>登陸title> <link rel="stylesheet" type="text/css" th:href="@{static/layui/css/layui.css}">head><body><form method="POST" action=""> <div> 用戶名:<input type="text" name="username" id="username"> div> <div> 密碼:<input type="password" name="password" id="password"> div> <div> <input type="button" name="login" id="login" th:value="當即登錄" onclick="mylogin()"> div>form><script type="text/javascript" charset="utf-8" th:src="@{static/jquery/jquery-3.5.1.min.js}">script><script type="text/javascript" charset="utf-8" th:src="@{static/layui/layui.js}">script><script th:inline="javascript" type="text/javascript"> layui.use(['form','jquery','layedit', 'laydate'], function () { var $ = layui.jquery, form = layui.form, layer = layui.layer; }); function mylogin() { var username = $("#username").val(); var password = $("#password").val(); console.log("username:" + username + "password:" + password); var index = layer.load(1); $.ajax({ type: "POST", dataType: "json", url: "user/login", data: { "username": username, "password": password //可加驗證碼參數等,後臺登錄處理LoginSuccessHandler中會傳入這些參數 }, success: function (data) { layer.close(index); console.log("data===>:" + JSON.stringify(data)); if (data.code == 200) { //登陸成功 window.location.href = "index"; } else { layer.msg(data.msg, { icon: 2, time: 3000 //2秒關閉(若是不配置,默認是3秒) }); } }, error: function () { layer.close(index); layer.msg("數據請求異常!", { icon: 2, time: 2000 //2秒關閉(若是不配置,默認是3秒) }); } }); }script>body>html>
4.3 演示圖片
登陸失敗
登陸成功
最後添加一個我寫的一個小demo,裏面也整合了security框架,使用springboot + ssm後端框架 + maven依賴包管理 + thmeleaf模板引擎 + pear-admin-layui前端框架等。
demo演示地址:http://www.qnto.top/springfashionsys/login
demo只對數據分析頁面作了權限設置,只有admin纔可訪問。
轉載須要加連接哦,整理不易。
總結:實踐是檢驗真理的惟一標準,親測可用。