在企業項目開發中,對系統的安全和權限控制每每是必需的,常見的安全框架有 Spring Security、Apache Shiro 等。本文主要簡單介紹一下 Spring Security,再經過 Spring Boot 集成開一個簡單的示例。javascript
Spring Security 是一種基於 Spring AOP 和 Servlet 過濾器 Filter 的安全框架,它提供了全面的安全解決方案,提供在 Web 請求和方法調用級別的用戶鑑權和權限控制。css
Web 應用的安全性一般包括兩方面:用戶認證(Authentication)和用戶受權(Authorization)。html
用戶認證指的是驗證某個用戶是否爲系統合法用戶,也就是說用戶可否訪問該系統。用戶認證通常要求用戶提供用戶名和密碼,系統經過校驗用戶名和密碼來完成認證。前端
用戶受權指的是驗證某個用戶是否有權限執行某個操做。java
Spring Security 功能的實現主要是靠一系列的過濾器鏈相互配合來完成的。如下是項目啓動時打印的默認安全過濾器鏈(集成5.2.0):mysql
[ org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5054e546, org.springframework.security.web.context.SecurityContextPersistenceFilter@7b0c69a6, org.springframework.security.web.header.HeaderWriterFilter@4fefa770, org.springframework.security.web.csrf.CsrfFilter@6346aba8, org.springframework.security.web.authentication.logout.LogoutFilter@677ac054, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@51430781, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4203d678, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@625e20e6, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@19628fc2, org.springframework.security.web.session.SessionManagementFilter@471f8a70, org.springframework.security.web.access.ExceptionTranslationFilter@3e1eb569, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3089ab62 ]
詳細解讀能夠參考:http://www.javashuo.com/article/p-rgyffmcv-nn.htmljquery
用於存儲應用程序安全上下文(Spring Context)的詳細信息,如當前操做的用戶對象信息、認證狀態、角色權限信息等。默認狀況下,SecurityContextHolder
會使用 ThreadLocal
來存儲這些信息,意味着安全上下文始終可用於同一執行線程中的方法。git
由於身份信息與線程是綁定的,因此能夠在程序的任何地方使用靜態方法獲取用戶信息。例如獲取當前通過身份驗證的用戶的名稱,代碼以下:github
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
其中,getAuthentication()
返回認證信息,getPrincipal()
返回身份信息,UserDetails
是對用戶信息的封裝類。web
認證信息接口,集成了 Principal
類。該接口中方法以下:
接口方法 | 功能說明 |
---|---|
getAuthorities() | 獲取權限信息列表,默認是 GrantedAuthority 接口的一些實現類,一般是表明權限信息的一系列字符串 |
getCredentials() | 獲取用戶提交的密碼憑證,用戶輸入的密碼字符竄,在認證事後一般會被移除,用於保障安全 |
getDetails() | 獲取用戶詳細信息,用於記錄 ip、sessionid、證書序列號等值 |
getPrincipal() | 獲取用戶身份信息,大部分狀況下返回的是 UserDetails 接口的實現類,是框架中最經常使用的接口之一 |
認證管理器,負責驗證。認證成功後,AuthenticationManager
返回一個填充了用戶認證信息(包括權限信息、身份信息、詳細信息等,但密碼一般會被移除)的 Authentication
實例。而後再將 Authentication
設置到 SecurityContextHolder
容器中。
AuthenticationManager
接口是認證相關的核心接口,也是發起認證的入口。但它通常不直接認證,其經常使用實現類 ProviderManager
內部會維護一個 List<AuthenticationProvider>
列表,存放裏多種認證方式,默認狀況下,只須要經過一個 AuthenticationProvider
的認證,就可被認爲是登陸成功。
負責從特定的地方加載用戶信息,一般是經過JdbcDaoImpl
從數據庫加載實現,也能夠經過內存映射InMemoryDaoImpl
實現。
該接口表明了最詳細的用戶信息。該接口中方法以下:
接口方法 | 功能說明 |
---|---|
getAuthorities() | 獲取授予用戶的權限 |
getPassword() | 獲取用戶正確的密碼,這個密碼在驗證時會和 Authentication 中的 getCredentials() 作比對 |
getUsername() | 獲取用於驗證的用戶名 |
isAccountNonExpired() | 指示用戶的賬戶是否已過時,沒法驗證過時的用戶 |
isAccountNonLocked() | 指示用戶的帳號是否被鎖定,沒法驗證被鎖定的用戶 |
isCredentialsNonExpired() | 指示用戶的憑據(密碼)是否已過時,沒法驗證憑證過時的用戶 |
isEnabled() | 指示用戶是否被啓用,沒法驗證被禁用的用戶 |
本文主要使用 Spring Security 來實現系統頁面的權限控制和安全認證,本示例不作詳細的數據增刪改查,sql 能夠在完整代碼裏下載,主要是基於數據庫對頁面 和 ajax 請求作權限控制。
t_user 用戶表
字段 | 類型 | 長度 | 是否爲空 | 說明 |
---|---|---|---|---|
id | int | 8 | 否 | 主鍵,自增加 |
username | varchar | 20 | 否 | 用戶名 |
password | varchar | 255 | 否 | 密碼 |
t_role 角色表
字段 | 類型 | 長度 | 是否爲空 | 說明 |
---|---|---|---|---|
id | int | 8 | 否 | 主鍵,自增加 |
role_name | varchar | 20 | 否 | 角色名稱 |
t_menu 菜單表
字段 | 類型 | 長度 | 是否爲空 | 說明 |
---|---|---|---|---|
id | int | 8 | 否 | 主鍵,自增加 |
menu_name | varchar | 20 | 否 | 菜單名稱 |
menu_url | varchar | 50 | 是 | 菜單url(Controller 請求路徑) |
t_user_roles 用戶權限表
字段 | 類型 | 長度 | 是否爲空 | 說明 |
---|---|---|---|---|
id | int | 8 | 否 | 主鍵,自增加 |
user_id | int | 8 | 否 | 用戶表id |
role_id | int | 8 | 否 | 角色表id |
t_role_menus 權限菜單表
字段 | 類型 | 長度 | 是否爲空 | 說明 |
---|---|---|---|---|
id | int | 8 | 否 | 主鍵,自增加 |
role_id | int | 8 | 否 | 角色表id |
menu_id | int | 8 | 否 | 菜單表id |
實體類這裏不詳細列了。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- 熱部署模塊 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <!-- 這個須要爲 true 熱部署纔有效 --> </dependency> <!-- mysql 數據庫驅動. --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- mybaits --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency> <!-- thymeleaf --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- alibaba fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <!-- spring security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
/** prePostEnabled :決定Spring Security的前註解是否可用 [@PreAuthorize,@PostAuthorize,..] secureEnabled : 決定是否Spring Security的保障註解 [@Secured] 是否可用 jsr250Enabled :決定 JSR-250 annotations 註解[@RolesAllowed..] 是否可用. */ @Configurable @EnableWebSecurity //開啓 Spring Security 方法級安全註解 @EnableGlobalMethodSecurity @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private CustomAccessDeniedHandler customAccessDeniedHandler; @Autowired private UserDetailsService userDetailsService; /** * 靜態資源設置 */ @Override public void configure(WebSecurity webSecurity) { //不攔截靜態資源,全部用戶都可訪問的資源 webSecurity.ignoring().antMatchers( "/", "/css/**", "/js/**", "/images/**", "/layui/**" ); } /** * http請求設置 */ @Override public void configure(HttpSecurity http) throws Exception { //http.csrf().disable(); //註釋就是使用 csrf 功能 http.headers().frameOptions().disable();//解決 in a frame because it set 'X-Frame-Options' to 'DENY' 問題 //http.anonymous().disable(); http.authorizeRequests() .antMatchers("/login/**","/initUserData")//不攔截登陸相關方法 .permitAll() //.antMatchers("/user").hasRole("ADMIN") // user接口只有ADMIN角色的能夠訪問 // .anyRequest() // .authenticated()// 任何還沒有匹配的URL只須要驗證用戶便可訪問 .anyRequest() .access("@rbacPermission.hasPermission(request, authentication)")//根據帳號權限訪問 .and() .formLogin() .loginPage("/") .loginPage("/login") //登陸請求頁 .loginProcessingUrl("/login") //登陸POST請求路徑 .usernameParameter("username") //登陸用戶名參數 .passwordParameter("password") //登陸密碼參數 .defaultSuccessUrl("/main") //默認登陸成功頁面 .and() .exceptionHandling() .accessDeniedHandler(customAccessDeniedHandler) //無權限處理器 .and() .logout() .logoutSuccessUrl("/login?logout"); //退出登陸成功URL } /** * 自定義獲取用戶信息接口 */ @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } /** * 密碼加密算法 * @return */ @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
public class UserEntity implements UserDetails { /** * */ private static final long serialVersionUID = -9005214545793249372L; private Long id;// 用戶id private String username;// 用戶名 private String password;// 密碼 private List<Role> userRoles;// 用戶權限集合 private List<Menu> roleMenus;// 角色菜單集合 private Collection<? extends GrantedAuthority> authorities; public UserEntity() { } public UserEntity(String username, String password, Collection<? extends GrantedAuthority> authorities, List<Menu> roleMenus) { this.username = username; this.password = password; this.authorities = authorities; this.roleMenus = roleMenus; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public List<Role> getUserRoles() { return userRoles; } public void setUserRoles(List<Role> userRoles) { this.userRoles = userRoles; } public List<Menu> getRoleMenus() { return roleMenus; } public void setRoleMenus(List<Menu> roleMenus) { this.roleMenus = roleMenus; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
/** * 獲取用戶相關信息 * @author charlie * */ @Service public class UserDetailServiceImpl implements UserDetailsService { private Logger log = LoggerFactory.getLogger(UserDetailServiceImpl.class); @Autowired private UserDao userDao; @Autowired private RoleDao roleDao; @Autowired private MenuDao menuDao; @Override public UserEntity loadUserByUsername(String username) throws UsernameNotFoundException { // 根據用戶名查找用戶 UserEntity user = userDao.getUserByUsername(username); System.out.println(user); if (user != null) { System.out.println("UserDetailsService"); //根據用戶id獲取用戶角色 List<Role> roles = roleDao.getUserRoleByUserId(user.getId()); // 填充權限 Collection<SimpleGrantedAuthority> authorities = new HashSet<SimpleGrantedAuthority>(); for (Role role : roles) { authorities.add(new SimpleGrantedAuthority(role.getRoleName())); } //填充權限菜單 List<Menu> menus=menuDao.getRoleMenuByRoles(roles); return new UserEntity(username,user.getPassword(),authorities,menus); } else { System.out.println(username +" not found"); throw new UsernameNotFoundException(username +" not found"); } } }
/** * RBAC數據模型控制權限 * @author charlie * */ @Component("rbacPermission") public class RbacPermission{ private AntPathMatcher antPathMatcher = new AntPathMatcher(); public boolean hasPermission(HttpServletRequest request, Authentication authentication) { Object principal = authentication.getPrincipal(); boolean hasPermission = false; if (principal instanceof UserEntity) { // 讀取用戶所擁有的權限菜單 List<Menu> menus = ((UserEntity) principal).getRoleMenus(); System.out.println(menus.size()); for (Menu menu : menus) { if (antPathMatcher.match(menu.getMenuUrl(), request.getRequestURI())) { hasPermission = true; break; } } } return hasPermission; } }
自定義處理無權請求
/** * 處理無權請求 * @author charlie * */ @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { private Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class); @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { boolean isAjax = ControllerTools.isAjaxRequest(request); System.out.println("CustomAccessDeniedHandler handle"); if (!response.isCommitted()) { if (isAjax) { String msg = accessDeniedException.getMessage(); log.info("accessDeniedException.message:" + msg); String accessDenyMsg = "{\"code\":\"403\",\"msg\":\"沒有權限\"}"; ControllerTools.print(response, accessDenyMsg); } else { request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); response.setStatus(HttpStatus.FORBIDDEN.value()); RequestDispatcher dispatcher = request.getRequestDispatcher("/403"); dispatcher.forward(request, response); } } } public static class ControllerTools { public static boolean isAjaxRequest(HttpServletRequest request) { return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); } public static void print(HttpServletResponse response, String msg) throws IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(msg); writer.flush(); writer.close(); } } }
登陸/退出跳轉
/** * 登陸/退出跳轉 * @author charlie * */ @Controller public class LoginController { @GetMapping("/login") public ModelAndView login(@RequestParam(value = "error", required = false) String error, @RequestParam(value = "logout", required = false) String logout) { ModelAndView mav = new ModelAndView(); if (error != null) { mav.addObject("error", "用戶名或者密碼不正確"); } if (logout != null) { mav.addObject("msg", "退出成功"); } mav.setViewName("login"); return mav; } }
登陸成功跳轉
@Controller public class MainController { @GetMapping("/main") public ModelAndView toMainPage() { //獲取登陸的用戶名 Object principal= SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String username=null; if(principal instanceof UserDetails) { username=((UserDetails)principal).getUsername(); }else { username=principal.toString(); } ModelAndView mav = new ModelAndView(); mav.setViewName("main"); mav.addObject("username", username); return mav; } }
用於不一樣權限頁面訪問測試
/** * 用於不一樣權限頁面訪問測試 * @author charlie * */ @Controller public class ResourceController { @GetMapping("/publicResource") public String toPublicResource() { return "resource/public"; } @GetMapping("/vipResource") public String toVipResource() { return "resource/vip"; } }
用於不一樣權限ajax請求測試
/** * 用於不一樣權限ajax請求測試 * @author charlie * */ @RestController @RequestMapping("/test") public class HttptestController { @PostMapping("/public") public JSONObject doPublicHandler(Long id) { JSONObject json = new JSONObject(); json.put("code", 200); json.put("msg", "請求成功" + id); return json; } @PostMapping("/vip") public JSONObject doVipHandler(Long id) { JSONObject json = new JSONObject(); json.put("code", 200); json.put("msg", "請求成功" + id); return json; } }
登陸頁面
<form class="layui-form" action="/login" method="post"> <div class="layui-input-inline"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> <input type="text" name="username" required placeholder="用戶名" autocomplete="off" class="layui-input"> </div> <div class="layui-input-inline"> <input type="password" name="password" required placeholder="密碼" autocomplete="off" class="layui-input"> </div> <div class="layui-input-inline login-btn"> <button id="btnLogin" lay-submit lay-filter="*" class="layui-btn">登陸</button> </div> <div class="form-message"> <label th:text="${error}"></label> <label th:text="${msg}"></label> </div> </form>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> 防止跨站請求僞造(CSRF)攻擊
退出系統
<form id="logoutForm" action="/logout" method="post" style="display: none;"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> </form> <a href="javascript:document.getElementById('logoutForm').submit();">退出系統</a>
ajax 請求頁面
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" id="hidCSRF"> <button class="layui-btn" id="btnPublic">公共權限請求按鈕</button> <br> <br> <button class="layui-btn" id="btnVip">VIP權限請求按鈕</button> <script type="text/javascript" th:src="@{/js/jquery-1.8.3.min.js}"></script> <script type="text/javascript" th:src="@{/layui/layui.js}"></script> <script type="text/javascript"> layui.use('form', function() { var form = layui.form; $("#btnPublic").click(function(){ $.ajax({ url:"/test/public", type:"POST", data:{id:1}, beforeSend:function(xhr){ xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val()); }, success:function(res){ alert(res.code+":"+res.msg); } }); }); $("#btnVip").click(function(){ $.ajax({ url:"/test/vip", type:"POST", data:{id:2}, beforeSend:function(xhr){ xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val()); }, success:function(res){ alert(res.code+":"+res.msg); } }); }); }); </script>
測試提供兩個帳號:user 和 admin (密碼與帳號同樣)
因爲 admin 做爲管理員權限,設置了所有的訪問權限,這裏只展現 user 的測試結果。
非特殊說明,本文版權歸 朝霧輕寒 全部,轉載請註明出處.
原文標題:Spring Boot 2.X(十八):集成 Spring Security-登陸認證和權限控制
原文地址: https://www.zwqh.top/article/info/27
若是文章有不足的地方,歡迎提點,後續會完善。
若是文章對您有幫助,請給我點個贊,請掃碼關注下個人公衆號,文章持續更新中...
原文出處:https://www.cnblogs.com/zwqh/p/11934880.html