為何學習spring security? 理由如下:
1)雖然可以不用,但難免部分客戶又要求
2)某種程度上,security還是不錯的,譬如csrf,oauth等等,省了一些功夫。
3)雖然spring security 比較龐雜,甚至有些臃腫,但權衡之下,還是可以一學!。
根據很多網絡例子和書籍來試驗,都沒有成功,原因可能是:
1)某些地方配置錯了
2)使用的版本和他人不同
費了不少功夫。
一氣之下,直接使用2.3.4的版本,成功了!
毫無疑問,2.3.4比以往的更加人性化。
以下內容比較長,可能需要耗費10分鍾以上時間閱讀。
自定義的關鍵在於幾點。
一、pom配置+spring配置
<?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.3.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>learning</groupId> <artifactId>secutiry</artifactId> <version>0.0.1-SNAPSHOT</version> <name>secutiry</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <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.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </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> <!-- 支持jsp --> <!-- 添加 servlet 依賴. --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <!-- 添加 JSTL(JSP Standard Tag Library,JSP標准標簽庫) --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <!-- Jasper是tomcat中使用的JSP引擎,運用tomcat-embed-jasper可以將項目與tomcat分開 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
#熱部署 spring.devtools.livereload.enabled=true spring.devtools.restart.enabled=true #服務器 server.port=8888 #bean 相關 #bean延遲啟動 spring.main.lazy-initialization=false #安全 #spring.security.user.name=root #spring.security.user.password=root server.servlet.context-path = / #優雅關閉--等待還有的連接完成,之后不再允許有新的請求,類似於一些數據庫的操作 server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=20s #設置靜態資源等 spring.mvc.static-path-pattern=/** spring.resources.static-locations=/css/,/images/,/WEB-INF/plugin/ #啟動jsp功能 spring.mvc.view.prefix=/WEB-INF/jsp/ spring.mvc.view.suffix=.jsp # 數據庫連接(mysql) spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://127.0.0.1:7799/spring?rewriteBatchedStatements=true&autoReconnect=true&allowMultiQueries=true&useSSL=false&serverTimezone=CST&allowPublicKeyRetrieval=true spring.datasource.username =lzf spring.datasource.password =123 #連接池配置-HikariCp----------------------------------------------------- #鑒於springboot目前的版本,spring.datasource.type也可以不寫 spring.datasource.name=hcmdmserverDs spring.datasource.type=com.zaxxer.hikari.HikariDataSource #是否自動提交,默認true spring.datasource.hikari.autoCommit=false #連接超時,過了這個時間還連接不到,hikari會返回錯誤,設置3分鍾 spring.datasource.hikari.connectionTimeout=180000 #多了多少毫秒不用,會被設置為空閑,默認是10分鍾 spring.datasource.hikari.idleTimeout=600000 #最大生命周期,默認1800000(30分鍾) spring.datasource.hikari.maxLifetime=1800000 #最少空閑連接 spring.datasource.hikari.minimumIdle=3 #最大連接池大小=空閑+在用 spring.datasource.hikari.maximumPoolSize=6 spring.datasource.hikari.connection-test-query=select 1 #如果不指定 spring.datasource.type,則以下可以是通用的連接池配置信息 #spring-jdbc #http-請求連接池
二、org.springframework.security.core.userdetails.UserDetailsService實現類
這個部分的關鍵是兩點:
1)實現UserDetailsService的時候,返回一個UserDetails即可
2)用戶密碼不需要像一些地方說的那樣要有{bcrypt}之類的前綴
/** * * @author lzfto * @apiNote */ @Service public class UdsDetail implements UserDetailsService { @Autowired MyUserService usersService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserRolePojo ur = usersService.getUserRoleDetail(username); return change(ur); } private UserDetails change(UserRolePojo ur) { List<GrantedAuthority> authorityList = new ArrayList<GrantedAuthority>(); List<RolePojo> roleList = ur.getRoleList(); for (RolePojo role : roleList) { GrantedAuthority gt = new SimpleGrantedAuthority(role.getName()); authorityList.add(gt); } UserDetails user = new User(ur.getUserPojo().getName(), ur.getUserPojo().getPassword(), authorityList); return user; } }
至於如何和數據庫關聯,還是比較簡單的,不需贅述!
此處附上插入用戶信息的腳本:
-- 學生表 CREATE TABLE `users` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(40) NOT NULL, `password` varchar(70) NOT NULL, `create_time` varchar(20) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; create table role( id int not null auto_increment, name varchar(40) not null, primary key(id) ); create table user_role_detail( id int not null auto_increment, user_id int not null, role_id int not null, primary key(id) ); create unique index uid_user_role_detail on user_role_detail(user_id,role_id); -- -- 密碼是123 -- 不需要添加什么 bcrypt之類的前綴,在2.3.4版本中。 INSERT INTO `spring`.`users` (`name`, `password`, `create_time`) VALUES ('lzf', '$2a$10$Mg8XzxbqsOMQAxrPD8d9hOELzDyGc7lShVdSb7vOLWwEplWlga7cO', '2020-08-11 12:00:00'); INSERT INTO `spring`.`users` (`name`, `password`, `create_time`) VALUES ('wth', '$2a$10$Mg8XzxbqsOMQAxrPD8d9hOELzDyGc7lShVdSb7vOLWwEplWlga7cO', '2020-08-11 12:00:00'); -- INSERT INTO `spring`.`role` (`name`) VALUES ('ADMIN'); INSERT INTO `spring`.`role` (`name`) VALUES ('MANAGER'); INSERT INTO `spring`.`role` (`name`) VALUES ('LEADER'); INSERT INTO `spring`.`role` (`name`) VALUES ('CEO'); -- insert into user_role_detail(user_id,role_id) select u.id,r.id from users u,role r where u.name='lzf'; insert into user_role_detail(user_id,role_id) select u.id,r.id from users u,role r where u.name='wth' and r.name!='ADMIN';
三、WebSecurityConfigurerAdapter等有關配置
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UdsDetail uService; @Override protected void configure(HttpSecurity http) throws Exception { /** * 1.需要把loginPage,loginProcessingUrl放開 permiAll,否則會進入無限循環重定向 * 2.antMatchers("/doLogin", "/touch","/error404").permitAll()的順序不重要 * 3.無需要配置 scanBasePackages */ http .formLogin() .loginPage("/doLogin") .loginProcessingUrl("/touch") .and() .authorizeRequests() .antMatchers("/doLogin", "/touch","/error404","/plugin/*").permitAll() .anyRequest().authenticated() .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder encoder = new BCryptPasswordEncoder(); auth.userDetailsService(uService).passwordEncoder(encoder); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/plugin/**", "/css/**", "/images/**"); } }
第一次看別人的"configure(HttpSecurity http)"方法的時候,
總有疑問:
- "loginProcessingUrl"是不是一定是/login?
- 是不是可以是其它的?
- 就算是/login,那么是否需要在控制器中定義一個對應的方法?
明確的答案見最后一個小節:“創建login.jsp”,此處不再贅述。
為了讓系統找到/doLogin,必須定義一個控制器方法
@RequestMapping("/doLogin") public ModelAndView skipLogin() { ModelAndView mv = new ModelAndView("login"); return mv; }
有的人不喜歡使用控制器,直接使用某個頁面替代,譬如的login.jsp。這就是存粹的個人習慣了!
應用啟動類:
@SpringBootApplication public class SecutiryApplication { public static void main(String[] args) { SpringApplication.run(SecutiryApplication.class, args); } }
SpringBootApplication無需具有 scanBasePackages的語法,因為那樣會導致springboot使用默認的WebSecurityConfigurerAdapter 覆蓋用戶自己自定義的類,譬如上文的WebSecurityConfig
四、模板引擎,個人推薦使用jsp
為什么使用springboot+jsp,是因為springboot搭建mvc的確方便,其次jsp是公司大部分人都會,都熟悉的語言,而thymeleaf之類的模板引擎
額外增加了學習成本,但我們的項目並沒有那么cloud。
所以綜合起來,使用springboot+jsp是不錯的
五、目錄結構
在src/main/下創建:
/src/main/webapp/WEB-INF
/src/main/webapp/WEB-INF/jsp (放jsp文件)
/src/main/webapp/plugin (放jsp第三方插件,譬如jquery,vue,bootstrap等)
/src/main/webapp/css (放公共樣式)
/src/main/webapp/resource (放圖片等資源)
六、創建登錄頁面login.jsp(放在/src/main/webapp/WEB-INF/jsp下面)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE html> <html > <head> <meta charset="UTF-8"> <title>登錄測試頁面(jsp)</title> </head> <body> <h2>自定義登錄表單(jsp)</h2> <div> <br /> <!-- action 叫什么無所謂,只需要和 WebSecurityConfigurerAdapter 中的 loginProcessingUrl 值一致即可 --> <form method="post" action="/touch"> 用戶名: <input type="text" id="username" name="username" placeholder="name"><br /> 密碼: <input type="password" id="password" name="password" placeholder="password"><br /> <input type="button" value="提交" onclick="fnLogin()"> <!-- <input type="submit" value="提交" > --> </form> </div> </body> <script type="text/javascript" src="/plugin/jquery-3.4.1.min.js"></script> <script> function fnLogin(){ let uName=$("#username").val(); let pwd=$("#password").val(); $.ajax({ method: "post", url: "/login", cache: false, async: false, data: { "username":uName, "password":pwd }, success: function (data) { //location.href ="/test/main"; alert("good"); }, error: function (data) { console.log(data); } }); } </script> </html>
如果想使用form提交,那么,提交按鈕如下設置:
<!-- <input type="button" value="提交" onclick="fnLogin()"> --> <input type="submit" value="提交" >
反之,如果想使用ajax請求,那么對上面的語句反向注釋即可:
<input type="button" value="提交" onclick="fnLogin()"> <!-- <input type="submit" value="提交" > -->
使用ajax請求的關鍵在於設定參數和url。
注:如果需要這么使用,必須保證先繼承UsernamePasswordAuthenticationFilter或者那個抽象父類AbstractAuthenticationProcessingFilter ,改寫有關內容。
如果不是很有必要,就還是老老實實使用默認的form提交。
而url在沒有修改的情況下是默認指定為/login,這是 在 UsernamePasswordAuthenticationFilter已經定義了,如下文:
/* * @author Ben Alex * @author Colin Sampaleanu * @author Luke Taylor * @since 3.0 */ public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // ===================================================================================== public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; private boolean postOnly = true; // ~ Constructors // =================================================================================================== public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); }
那么是否可以不用/login? 答案是可以的!
怎么做? 繼承AbstractAuthenticationProcessingFilter ,然后在自定義的WebSecurityConfigurerAdapter 中配置一個新的過濾器。
假定這個繼承的過濾器叫MyAuthFilter,那么MyAuthFilter在完成驗證之后,就直接繞過原有的UsernamePasswordAuthenticationFilter等后續驗證。
當然這樣的做法並不是太好!
但這是為了告訴我們驗證url是可以修改的,而不必都是/login。