1.前言
前面學習了 security的登錄與登出 , 但是用戶信息 是 application 配置 或內存直接注入進去的 ,不具有實用性,實際上的使用還需要權限管理,有些 訪問接口需要某些權限才可以使用
於是多了個權限管理的問題
2.環境
spring boot 2.1.6.RELEASE
mysql 5.5.28*win64
jdk 1.8.0_221
3.操作
(1)准備一張MySQL表
CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵,自遞增', `username` varchar(20) DEFAULT NULL COMMENT '用戶名', `psw` varchar(140) DEFAULT NULL COMMENT '密碼', `nickname` varchar(50) DEFAULT NULL COMMENT '別名', `role` varchar(100) DEFAULT NULL COMMENT '權限名', `setTime` datetime DEFAULT NULL COMMENT '注冊時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;
(2)目錄結構
(3)pom.xml

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>security-5500</artifactId> <version>0.0.1-SNAPSHOT</version> <name>security-5500</name> <description>Demo project for Spring Boot</description> <properties> <!-- 設置項目編碼格式--> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!--spring security 依賴--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--訪問靜態資源--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- MySQL 依賴--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <!-- <scope>runtime</scope>--> <version>5.1.30</version> </dependency> <!--MySQL 數據源 依賴包--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!-- mybatis依賴--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.2</version> </dependency> <!-- mybatis的逆向工程依賴包--> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> <!-- SCryptPasswordEncoder 加密才需要使用--> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.64</version> </dependency> <!--java工具包--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </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.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
(4)配置mybatis 與 dao層接口【具體操作這里不演示,可看我的其他隨筆有具體講解】
(5)配置前端頁面
index.html

<!DOCTYPE html> <html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <meta charset="UTF-8"> <title>index</title> </head> <body> 你好 ,世界 ,2333 <p>點擊 <a th:href="@{/home}">我</a> 去home.html頁面</p> </body> </html>
home.html

<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>security首頁</title>
</head>
<body>
<h1>Welcome!你好,世界</h1>
<p>Click <a th:href="@{/hai}">here</a> to see a greeting.</p>
</body>
</html>
hai.html

<!DOCTYPE html> <html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <meta charset="UTF-8"> <title>hai文件</title> </head> <body> 你好呀世界,成功登錄進來了 <br> <hr> 用戶名:<span th:text="${username}"></span> <hr> <!-- 登出 路徑是在security 攔截規則 那 設置的 ,當然也可以使用自己寫的 ,必須post方式才可以訪問,因為默認開啟了CSRF --> <form th:action="@{/mylogout}" method="post"> <button class="btn btn-danger" style="margin-top: 20px">退出登錄</button> </form> </body> </html>
kk.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>kk</title> </head> <body> <img src="img/xx.png" alt=""> </body> </html>
login.html

<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Spring Security自定義</title> </head> <body> <div th:if="${param.error}"> Invalid username and password. </div> <div th:if="${param.logout}"> You have been logged out. </div> <form th:action="@{/login}" method="post"> <div><label> User Name : <input type="text" name="username"/> </label></div> <div><label> Password: <input type="password" name="password"/> </label></div> <div><input type="submit" value="Sign In"/></div> </form> <br> lalallalalal啊是德國海 </body> </html>
(6)配置controller 虛擬路徑 【訪問接口】

package com.example.security5500.controller; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import java.security.Principal; @Controller public class MVCController { @RequestMapping("/home") public String home() { return "home"; } @RequestMapping("/login") public String login(){ return "login"; } @RequestMapping("/hai") public String hai(@AuthenticationPrincipal Principal principal, Model model) { //獲取登錄用戶名信息 ,如果沒有登錄 principal.getName() 會報異常,因此弄個異常拋出 String s= "r"; try { if (principal.getName() !=null){ s = principal.getName(); } }catch (Exception e){ System.out.println("principal.getName()出異常"); } model.addAttribute("username", s); return "hai"; } @RequestMapping({"/", "/index"}) public String index() { return "index"; } @RequestMapping("kk") public String kk() { return "kk"; } //獲取用戶權限 @RequestMapping({"/info"}) @ResponseBody public Object info(@AuthenticationPrincipal Principal principal) { return principal; } /* {"authorities":[{"authority":"admin"},{"authority":"user"}], "details":{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"1F57B8E39C5D1DB1F875D57D533DB982"}, "authenticated":true,"principal":{"password":null,"username":"xi","authorities":[{"authority":"admin"}, {"authority":"user"}],"accountNonExpired":true,"accountNonLocked":true, "credentialsNonExpired":true,"enabled":true},"credentials":null,"name":"xi"} */ }

package com.example.security5500.controller; import com.example.security5500.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.security.Principal; import java.util.Map; @Controller @RequestMapping("/admin") public class UserController { @Autowired private UserService userService; // //登出操作 // @RequestMapping({"/lo"}) // public String logout(HttpServletRequest request, HttpServletResponse response) { // Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // if (auth != null) {//清除認證 // new SecurityContextLogoutHandler().logout(request, response, auth); // } // //重定向到指定頁面 // return "redirect:/login"; // } //添加用戶 @RequestMapping({"/addUser"}) @ResponseBody public Map<String,Object> addUser(String username , String psw ) { return userService.addUser(username,psw); } }
(7)service層實現類

package com.example.security5500.service.serviceImpl; import com.example.security5500.dao.TUserMapper; import com.example.security5500.entitis.tables.TUser; import com.example.security5500.service.UserService; import org.apache.commons.lang3.StringUtils; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Date; import java.util.HashMap; import java.util.Map; @Service public class UserServiceImpl implements UserService { @Resource private TUserMapper tUserMapper; //根據用戶名獲取用戶信息 @Override public TUser getByUsername(String useranme) { return tUserMapper.selectByUsername(useranme); } //添加新用戶 @Override public Map<String,Object> addUser(String username, String psw) { Map<String,Object> map = new HashMap<>(); if (StringUtils.isBlank(username) || StringUtils.isBlank(psw)) { map.put("data","參數不可空"); return map; } ////根據用戶名獲取用戶信息 TUser u = tUserMapper.selectByUsername(username); if (u!= null){ map.put("data","用戶名已經存在"); return map; } // TUser tUser = new TUser(); tUser.setUsername(username); // //BCryptPasswordEncoder 單向加密 tUser.setPsw((new BCryptPasswordEncoder()).encode(psw)); // tUser.setNickname("別名-昵稱"); tUser.setRole("user"); tUser.setSettime(new Date()); int len = tUserMapper.insertSelective(tUser); if (len!=1){ map.put("data","失敗"); }else { map.put("data","成功"); } return map; } }
(8)啟動類

package com.example.security5500; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @SpringBootApplication //設置mapper接口包位置 @MapperScan(basePackages = "com.example.security5500.dao") public class Security5500Application { public static void main(String[] args) { SpringApplication.run(Security5500Application.class, args); } }
(9)security配置類 ,繼承了 WebSecurityConfigurerAdapter ,重寫了父類方法 ,可對訪問路徑自定義設置攔截規則

package com.example.security5500.securityConfig; import org.springframework.beans.factory.annotation.Autowired; 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.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; //這個加不加無所謂 //@Configuration //開啟security自定義配置 @EnableWebSecurity //開啟 Controller層的訪問方法權限,與注解@PreAuthorize("hasRole('admin')")配合,但是 經測試,無法使用,前端訪問指定接口報錯403 , //@EnableGlobalMethodSecurity(prePostEnabled=true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //實例自定義登錄校驗接口 【內部有 數據庫查詢】 @Autowired private DbUserDetailsService dbUserDetailsService; //攔截規則設置 @Override protected void configure(HttpSecurity http) throws Exception { http //允許基於使用HttpServletRequest限制訪問 .authorizeRequests() //設置不攔截頁面,可直接通過,路徑訪問 "/", "/index", "/home" 則不攔截, .antMatchers("/", "/index", "/home", "/hhk/**") //是允許所有的意思 .permitAll() //訪問 /hai 需要admin權限 ,無權限則提示 403 .antMatchers("/hai").hasAuthority("admin") //訪問 /kk 需要admin或user權限 ,無權限則提示 403 .antMatchers("/kk").hasAnyAuthority("admin","user") //路徑/admin/**所有的請求都需要admin權限 ,無權限則提示 403 .antMatchers("/admin/**").hasAuthority("admin") //其他頁面都要攔截,【需要在最后設置這個】 .anyRequest().authenticated() .and() //設置自定義登錄頁面 .formLogin() //指定自定義登錄頁面的訪問虛擬路徑 .loginPage("/login") .permitAll() .and() // 添加退出登錄支持。當使用WebSecurityConfigurerAdapter時,這將自動應用。默認情況是,訪問URL”/ logout”,使HTTP Session無效 // 來清除用戶,清除已配置的任何#rememberMe()身份驗證,清除SecurityContextHolder,然后重定向到”/login?success” .logout() // //指定的登出操作的虛擬路徑,需要以post方式請求這個 http://localhost:5500/mylogout 才可以登出 ,也可以直接清除用戶認證信息達到登出目的 .logoutUrl("/mylogout") //登出成功后訪問的地址 .logoutSuccessUrl("/home"); } /** * 添加 UserDetailsService, 實現自定義登錄校驗,數據庫查詢 */ @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { //注入用戶信息,每次登錄都會來這查詢一次信息,因此不建議每次都向mysql查詢,應該使用redis //密碼加密 builder.userDetailsService(dbUserDetailsService).passwordEncoder(passwordEncoder()); } /** * BCryptPasswordEncoder相關知識: * 用戶表的密碼通常使用MD5等不可逆算法加密后存儲,為防止彩虹表破解更會先使用一個特定的字符串(如域名)加密,然后再使用一個隨機的salt(鹽值)加密。 * 特定字符串是程序代碼中固定的,salt是每個密碼單獨隨機,一般給用戶表加一個字段單獨存儲,比較麻煩。 * BCrypt算法將salt隨機並混入最終加密后的密碼,驗證時也無需單獨提供之前的salt,從而無需單獨處理salt問題。 */ @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // /** // * 選擇加密方式 ,密碼不加密的時候選擇 NoOpPasswordEncoder,不可缺少,否則報錯 // * java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" // */ // @Bean // public static PasswordEncoder passwordEncoder() { // return NoOpPasswordEncoder.getInstance(); // } }
(10)實現自定義登錄校驗,實現了根據用戶名去數據庫查詢用戶信息,集齊參數用戶名、加密后的密碼、權限 ,
然后使用 new org.springframework.security.core.userdetails.User(tUser.getUsername(), tUser.getPsw(), simpleGrantedAuthorities); 注冊登錄用戶 ,
然后內部會自動對比密碼 進行校驗 【使用 BCryptPasswordEncoder 單項加密】

package com.example.security5500.securityConfig; import com.example.security5500.entitis.tables.TUser; import com.example.security5500.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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 java.util.ArrayList; import java.util.List; @Service public class DbUserDetailsService implements UserDetailsService { @Autowired private UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根據用戶名查詢用戶信息 TUser tUser = userService.getByUsername(username); if (tUser == null){ throw new UsernameNotFoundException("用戶不存在!"); } //權限設置 // List<GrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); String role = tUser.getRole(); //分割權限名稱,如 user,admin String[] roles = role.split(","); System.out.println("添加權限"); for (String r :roles){ System.out.println(r); //添加權限 simpleGrantedAuthorities.add(new SimpleGrantedAuthority(r)); } // simpleGrantedAuthorities.add(new SimpleGrantedAuthority("USER")); /** * 創建一個用於認證的用戶對象並返回,包括:用戶名,密碼,角色 */ //輸入參數 return new org.springframework.security.core.userdetails.User(tUser.getUsername(), tUser.getPsw(), simpleGrantedAuthorities); } }
(11)application.properties

spring.application.name=security-5500 # 應用服務web訪問端口 server.port=5500 #配置security登錄賬戶密和密碼 ,不配置則默認賬戶是user,密碼是隨機生成的字符串,打印在啟動欄中 #spring.security.user.name=11 #spring.security.user.password=22 # ## ## ## ## Enable template caching. #spring.thymeleaf.cache=true ## Check that the templates location exists. #spring.thymeleaf.check-template-location=true ## Content-Type value. ##spring.thymeleaf.content-type=text/html ## Enable MVC Thymeleaf view resolution. #spring.thymeleaf.enabled=true ## Template encoding. #spring.thymeleaf.encoding=utf-8 ## Comma-separated list of view names that should be excluded from resolution. #spring.thymeleaf.excluded-view-names= ## Template mode to be applied to templates. See also StandardTemplateModeHandlers. #spring.thymeleaf.mode=HTML5 ## Prefix that gets prepended to view names when building a URL. ##設置html文件位置 #spring.thymeleaf.prefix=classpath:/templates/ ## Suffix that gets appended to view names when building a URL. #spring.thymeleaf.suffix=.html spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain. spring.thymeleaf.view-names= # Comma-separated list of view names that can be resolved. # # #設置mybatis #mybatis設置 #mybatis配置文件所在路徑 mybatis.config-location=classpath:mybatis/config/mybatisConfig.xml #所有Entity別名類所在包 mybatis.type-aliases-package=com.example.security5500.entitis.tables #mapper映射xml文件[也可以放在 resources 里面] #不論放在哪里,都必須使用classpath: 否則找不到 ,報錯 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): mybatis.mapper-locations= classpath:mybatis/mapper/**/*.xml #mysql配置 # 當前數據源操作類型 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource # mysql驅動包 spring.datasource.driver-class-name=org.gjt.mm.mysql.Driver # 數據庫名稱 spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf-8 # 數據庫賬戶名 spring.datasource.username=root # 數據庫密碼 spring.datasource.password=mysql # # # 數據庫連接池的最小維持連接數 spring.datasource.dbcp2.min-idle=5 # 初始化連接數 spring.datasource.dbcp2.initial-size=5 # 最大連接數 spring.datasource.dbcp2.max-total=5 # 等待連接獲取的最大超時時間 spring.datasource.dbcp2.max-wait-millis=200 # # 指明是否在從池中取出連接前進行檢驗,如果檢驗失敗, 則從池中去除連接並嘗試取出另一個, #注意: 設置為true后如果要生效,validationQuery參數必須設置為非空字符串 spring.datasource.druid.test-on-borrow=false # # 指明連接是否被空閑連接回收器(如果有)進行檢驗.如果檢測失敗,則連接將被從池中去除. #注意: 設置為true后如果要生效,validationQuery參數必須設置為非空字符串 spring.datasource.druid.test-while-idle=true # # 指明是否在歸還到池中前進行檢驗,注意: 設置為true后如果要生效, #validationQuery參數必須設置為非空字符串 spring.datasource.druid.test-on-return=false # # SQL查詢,用來驗證從連接池取出的連接,在將連接返回給調用者之前. #如果指定,則查詢必須是一個SQL SELECT並且必須返回至少一行記錄 spring.datasource.druid.validation-query=select 1
4.測試
(1)啟動 默認進入 index.html
點擊 “我” ,進入 home.html
點擊 “here” ,進入 hai.html ,但是因為設置了攔截,需要登錄才可以訪問 ,因此進入了自定義的登錄頁面
用一個只有 user權限的賬戶
username = cen
password = 11
登錄后顯示 403
因為我將訪問 hai.html的權限設為需要 admin 才可以訪問 ,因此拒絕操作
換一個有admin權限的賬戶
username = xi
password = 11
訪問網址http://localhost:5500/login
再次登錄
這是對一個終端訪問接口的權限攔截
那么,需要將某一路徑的請求都給攔截怎么辦?難道一個一個寫?
不,可以攔截上一層的虛擬路徑
security的的配置寫法
(2)一個攔截路徑可以設置多個權限,只要有任意一個權限都可以訪問
網址訪問 http://localhost:5500/kk ,【無權限仍然提示403】