前言
在過往的一些Spring Boot學(xué)習(xí)項(xiàng)目中饺饭,我們會發(fā)現(xiàn)渤早,我們開發(fā)的API都不需要認(rèn)證职车,對所有人開放,連登錄都不需要鹊杖,毫無安全可言悴灵。
在項(xiàng)目實(shí)戰(zhàn)中往往需要做好認(rèn)證、授權(quán)骂蓖、攻擊防護(hù)积瞒,Spring Boot在這方面也提供了快速解決方案,即:推薦使用Spring Security登下。
-
Spring Boot為Spring Security提供了自動化配置方案茫孔,可零配置使用 Spring Security。
項(xiàng)目代碼已上傳Git Hub被芳,歡迎取閱:
簡單入門
1. 添加依賴缰贝;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
2. 編寫Controller;
package com.github.dylanz666.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() throws Exception {
return "Hello!";
}
}
3. 啟動項(xiàng)目并訪問API畔濒;
- 啟動項(xiàng)目:
注意一條log:Using generated security password: e10ac5ca-d3ab-4f0e-8e25-cbcf6afce611剩晴,下文會使用到。
- 在瀏覽器中訪問API:
API如:http://127.0.0.1:8080/hello
-
輸入用戶名密碼登錄:
1). Username: 默認(rèn)用戶名為user;
2). Password: 默認(rèn)密碼為log中打印的密碼侵状,e10ac5ca-d3ab-4f0e-8e25-cbcf6afce611赞弥;
戶名密碼登錄
我們一起來分析一下:
1). 未登錄時訪問API會重定向到登錄頁面:http://127.0.0.1:8080/login;
2). Spring Security為我們提供了默認(rèn)的登錄頁面趣兄,登錄頁面還算美觀绽左;
3). 登錄后,后續(xù)的請求中艇潭,會在請求頭中帶上含有JESSIONID的Cookie拼窥;
可在項(xiàng)目application.properties中提前配置好用戶名和密碼,如:
server.port=8080
spring.security.user.name=dylanz
spring.security.user.password=666
至此暴区,我們就實(shí)現(xiàn)了最簡單的登錄認(rèn)證闯团。
自定義登錄頁面實(shí)例
- 未登錄狀態(tài)下API請求重定向到登錄頁面還是比較奇怪的,一般來說仙粱,API未登錄狀態(tài)下的請求應(yīng)該顯示狀態(tài)碼:401房交;
- 通常情況下,應(yīng)該是進(jìn)入某個有訪問限制的頁面伐割,當(dāng)未登錄時候味,重定向到登錄頁面刃唤;
因此,我們將場景變?yōu)椋?/h4>
- 主頁為(無需登錄即可訪問): http://127.0.0.1:8080/home.html
- 主頁提供訪問限制的入口白群;
- 有訪問限制的頁面為:http://127.0.0.1:8080/hello.html
- 登錄頁面為: http://127.0.0.1:8080/login.html
我們將采用視圖技術(shù)尚胞,簡單做個案例。Spring Boot框架內(nèi)使用視圖技術(shù)可參考:
thymeleaf使用準(zhǔn)備:
1). 添加thymeleaf依賴笼裳;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2). 修改配置文件;
server.port=8080
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
1. 定義主頁home.html粱玲;
- 在resources下創(chuàng)建templates文件夾躬柬,并創(chuàng)建home.html文件:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Click <a th:href="@{/hello.html}">here</a> to see a greeting.</p>
</body>
</html>
- 前往hello.html頁面的代碼:<a th:href="@{/hello.html}">here</a>
2. 定義hello.html頁面;
- 在templates文件夾下創(chuàng)建hello.html文件:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
- hello.html頁面上提供一個登出入口"Sign Out";
3. 自定義login/logout頁面抽减;
- 在templates文件夾下創(chuàng)建login.html文件:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </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 th:style="'background:red;'"> User Name: <input type="text" name="username"/> </label></div>
<div><label th:style="'background:red;'"> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
- 當(dāng)用戶名密碼錯誤時提示信息:Invalid username and password.
- 當(dāng)?shù)浅鰰r提示信息:You have been logged out.
- 為了演示自定義頁面允青,我還特地改了下頁面元素樣式,把User Name和Password label的背景色改為紅色:th:style="'background:red;'"
(筆者沒有花過多的時間處理樣式哈卵沉,此處只做簡單演示)
4. 組織頁面行為颠锉;
1). 配置模板匹配規(guī)則;
目的是使網(wǎng)站的url指向具體視圖史汗,而不是當(dāng)作API來訪問琼掠;
在項(xiàng)目下創(chuàng)建config包,并在config包內(nèi)創(chuàng)建WebMvcConfig類淹办,編寫WebMvcConfig類如下:
package com.github.dylanz666.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home.html").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello.html").setViewName("hello");
registry.addViewController("/login.html").setViewName("login");
}
}
- 訪問/和/home.html路徑時眉枕,使用模板:home.html;
- 訪問/hello.html路徑時怜森,使用模板:hello.html速挑;
- 訪問/login.html路徑時,使用模板:login.html副硅。
2). 頁面訪問權(quán)限設(shè)置姥宝;
在config包下創(chuàng)建類:WebSecurityConfig,編寫類如下:
package com.github.dylanz666.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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/", "/home.html").permitAll()//這2個url不用訪問認(rèn)證
.anyRequest().authenticated()//其他url都需要訪問認(rèn)證
.and()
.formLogin()
.loginPage("/login.html")//登錄頁面的url
.loginProcessingUrl("/login")//登錄表使用的API
.permitAll()//login.html和login不需要訪問認(rèn)證
.and()
.logout()
.permitAll();//logout不需要訪問認(rèn)證
}
}
幾點(diǎn)解釋:
- @EnableWebSecurity:官網(wǎng)說這是為了開啟Web Security支持恐疲,并提供Spring MVC集成腊满,具體咋回事咱也不知道呀,跟著用就是對了培己!
- .antMatchers("/", "/home.html").permitAll():配置不需要認(rèn)證的url碳蛋,也即任何人都可以訪問的url;
- .loginPage("/login.html"):配置登錄頁面的url省咨,由于我們自定義了登錄頁面肃弟,因此需使用這個配置,如果不是用此配置,則使用Spring Security提供的默認(rèn)登錄頁面笤受;
- .loginProcessingUrl("/login"): 配置登錄表單使用的API穷缤,Spring Security默認(rèn)提供"/login"接口,用于登錄驗(yàn)證箩兽;
3). 啟動項(xiàng)目查看效果津肛;
-
訪問主頁:http://127.0.0.1:8080/
-
點(diǎn)擊頁面中的"here"鏈接;
此時嘗試訪問http://127.0.0.1:8080/hello.html汗贫,但由于我們沒有登錄身坐,因此Spring Security自動幫我們跳轉(zhuǎn)到登錄頁面:http://127.0.0.1:8080/login.html
-
登錄;
-
登錄后訪問項(xiàng)目寫好的API芳绩;
筆者在項(xiàng)目中的controller包中寫了個HelloController類掀亥,類中寫了個get類型的API,代碼如下:
package com.github.dylanz666.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() throws Exception {
return "Hello!";
}
}
此時在瀏覽器中直接訪問API:http://127.0.0.1:8080/hello
-
登出妥色;
點(diǎn)擊hello.html頁面上的"Sign Out"按鈕登出;
此時退出到登錄頁面遏片,且頁面有提示信息:You have been logged out.
-
登出后訪問項(xiàng)目寫好的API嘹害;
再次在瀏覽器中直接訪問API:http://127.0.0.1:8080/hello
此時我們會發(fā)現(xiàn)API被重定向到登錄頁面了;
通過本案例吮便,我們學(xué)會了如何使用Spring Security進(jìn)行基本的訪問限制和自定義登錄頁面笔呀。
用戶管理;
用戶管理有幾種方式:
1. 在resources底下的application.properties內(nèi)配置可登錄的用戶信息:
spring.security.user.name=dylanz
spring.security.user.password=666
這種方式有個弊端:只能配置一個用戶信息髓需;
2. 在config底下的WebSecurityConfig配置類內(nèi)添加可登錄的用戶信息userDetailsService许师,如:
package com.github.dylanz666.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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/", "/home.html").permitAll()//這2個url不用訪問認(rèn)證
.anyRequest().authenticated()//其他url都需要訪問認(rèn)證
.and()
.formLogin()
.loginPage("/login.html")//登錄頁面的url
.loginProcessingUrl("/login")//登錄表使用的API
.permitAll()//login.html和login不需要訪問認(rèn)證
.and()
.logout()
.permitAll();//logout不需要訪問認(rèn)證
}
@Bean
@Override
public UserDetailsService userDetailsService() {
UserDetails dylanz =
User.withUsername("dylanz")
.password(bCryptPasswordEncoder.encode("666"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}
3. WebSecurityConfig配置類內(nèi)可配置多個可登錄的用戶信息:
package com.github.dylanz666.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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/", "/home.html").permitAll()//這2個url不用訪問認(rèn)證
.anyRequest().authenticated()//其他url都需要訪問認(rèn)證
.and()
.formLogin()
.loginPage("/login.html")//登錄頁面的url
.loginProcessingUrl("/login")//登錄表使用的API
.permitAll()//login.html和login不需要訪問認(rèn)證
.and()
.logout()
.permitAll();//logout不需要訪問認(rèn)證
}
@Bean
@Override
public UserDetailsService userDetailsService() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
UserDetails dylanz =
User.withUsername("dylanz")
.password(bCryptPasswordEncoder.encode("666"))
.roles("ADMIN")
.build();
UserDetails ritay =
User.withUsername("ritay")
.password(bCryptPasswordEncoder.encode("888"))
.roles("USER")
.build();
UserDetails jonathanw =
User.withUsername("jonathanw")
.password(bCryptPasswordEncoder.encode("999"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(dylanz, ritay, jonathanw);
}
}
我在WebSecurityConfig配置類內(nèi)設(shè)置了3個可登錄的用戶,我們可以通過這種方式相對靈活的添加N個用戶僚匆。
4. 在數(shù)據(jù)庫中保存可登錄的用戶信息:
這是更常見的保存用戶信息的方式微渠,我們?nèi)砸宰詈唵蔚姆绞絹鞤emo從中心化的用戶信息池獲取用戶信息,即:模擬數(shù)據(jù)庫查詢過程咧擂;
1). 項(xiàng)目下創(chuàng)建domain包逞盆、service包;
2). domain包內(nèi)創(chuàng)建User實(shí)體類松申、service包下創(chuàng)建UserDetailsImpl類和UserDetailsServiceImpl類云芦;
3). 編寫User實(shí)體類;
package com.github.dylanz666.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* @author : dylanz
* @since : 08/31/2020
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
}
4). 編寫UserDetailsImpl類贸桶;
package com.github.dylanz666.service;
import com.github.dylanz666.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
/**
* @author : dylanz
* @since : 08/31/2020
*/
@Service
public class UserDetailsImpl implements UserDetails {
private User currentUser;
public UserDetailsImpl() {
}
public UserDetailsImpl(User user) {
if (user != null) {
this.currentUser = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin");
authorities.add(authority);
return authorities;
}
@Override
public String getPassword() {
return currentUser.getPassword();
}
public String getUsername() {
return currentUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
5). 編寫UserDetailsServiceImpl類舅逸;
package com.github.dylanz666.service;
import com.github.dylanz666.domain.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author : dylanz
* @since : 08/31/2020
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserDetailsImpl userService;
@Autowired
private UserDetails userDetails;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//Spring Security要求必須加密密碼
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//模擬從數(shù)據(jù)庫中取出用戶信息,使用的sql如: SELECT * FROM USER WHERE USER_NAME='cherrys'
List<User> userList = new ArrayList<>();
User firstUser = new User();
firstUser.setUsername("cherrys");
firstUser.setPassword(passwordEncoder.encode("123"));
userList.add(firstUser);
User secondUser = new User();
secondUser.setUsername("randyh");
secondUser.setPassword(passwordEncoder.encode("456"));
userList.add(secondUser);
List<User> mappedUsers = userList.stream().filter(s -> s.getUsername().equals(username)).collect(Collectors.toList());
//判斷用戶是否存在
User user;
if (CollectionUtils.isEmpty(mappedUsers)) {
logger.info(String.format("The user %s is not found !", username));
throw new UsernameNotFoundException(String.format("The user %s is not found !", username));
}
user = mappedUsers.get(0);
return new UserDetailsImpl(user);
}
}
解釋一下:
-
UserDetailsServiceImpl: 用于模擬從數(shù)據(jù)庫查詢出用戶信息皇筛,且模擬數(shù)據(jù)庫中存儲了加密的字符串琉历;
-
UserDetailsImpl:用于使用從數(shù)據(jù)庫查詢出的用戶信息,設(shè)置可登錄的用戶名设联、密碼善已,設(shè)置過程要配合使用WebSecurityConfig灼捂;
6). 修改WebSecurityConfig配置類;
package com.github.dylanz666.config;
import com.github.dylanz666.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author : dylanz
* @since : 08/30/2020
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/", "/home.html").permitAll()//這2個url不用訪問認(rèn)證
.anyRequest().authenticated()//其他url都需要訪問認(rèn)證
.and()
.formLogin()
.loginPage("/login.html")//登錄頁面的url
.loginProcessingUrl("/login")//登錄表使用的API
.permitAll()//login.html和login不需要訪問認(rèn)證
.and()
.logout()
.permitAll();//logout不需要訪問認(rèn)證
httpSecurity.userDetailsService(userDetailsService());
httpSecurity.userDetailsService(userDetailsService);
}
@Bean
@Override
public UserDetailsService userDetailsService() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
UserDetails dylanz =
User.withUsername("dylanz")
.password(bCryptPasswordEncoder.encode("666"))
.roles("ADMIN")
.build();
UserDetails ritay =
User.withUsername("ritay")
.password(bCryptPasswordEncoder.encode("888"))
.roles("USER")
.build();
UserDetails jonathanw =
User.withUsername("jonathanw")
.password(bCryptPasswordEncoder.encode("999"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(dylanz, ritay, jonathanw);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
特別注意:
-
必須在WebSecurityConfig中聲明PasswordEncoder换团;
-
在WebSecurityConfig的configure方法中使用:
httpSecurity.userDetailsService(userDetailsService);
至此悉稠,我們在內(nèi)存中添加了dylanz,ritay艘包,jonathanw三個用戶的猛,并且數(shù)據(jù)庫中也存儲了cherrys、randyh兩個用戶想虎,一共5個用戶卦尊;
我們來測試一下:
這個認(rèn)證過程還是比較初級的,真實(shí)案例中會比這個認(rèn)證過程復(fù)雜許多舌厨,我們開卷有益岂却,再接再厲!
如果本文對您有幫助裙椭,麻煩動動手指點(diǎn)點(diǎn)贊躏哩?
謝謝!