工具:idea2018,springboot 2.1.4,springsecurity 5.1.5html
SpringSecurity是Spring下的一個安全框架,與shiro 相似,通常用於用戶認證(Authentication)和用戶受權(Authorization)兩個部分,常與與SpringBoot相整合。前端
便於理解,下一節再使用先後端分離,並引入數據庫用戶和角色信息java
(pom.xml)web
<dependencies>
<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.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
複製代碼
(controller.UserController)算法
@Controller
public class UserController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "hello controller";
}
}
複製代碼
啓動項目,瀏覽器訪問:localhost:8080/hello,地址欄自動跳轉到http://localhost:8080/login,進入默認登錄頁面,驗證登陸spring
Username默認爲user
,Password隨機生成(實際就是UUID),查看控制檯。數據庫
Spring Security默認進行URL訪問進行攔截,並提供了驗證的登陸頁面json
輸入密碼,我這裏目前是c1068cdb-18f3-48f4-b838-7698218d14c4
。登陸成功後端
這裏的用戶名和密能夠修改,直接在配置文件中修改登陸名和密碼,如數組
(application.properties)
spring.security.user.name=admin
spring.security.user.password=123
複製代碼
1> 用戶參數
參照源碼,查看靜態內部類。能夠看出,默認用戶的密碼實際就是一個UUID。
(SpringSecurity -- SecurityProperties.java)
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
...
// 默認用戶
private User user = new User();
...
public static class User {
// 默認用戶名
private String name = "user";
// 默認用戶名的默認密碼,隨機生成
private String password = UUID.randomUUID().toString();
// 默認用戶名的角色
private List<String> roles = new ArrayList<>();
// 是否生成密碼
private boolean passwordGenerated = true;
...
}
}
複製代碼
2> 用戶名密碼驗證
導入security依賴後,默認訪問的路徑將通過該過濾器,並訪問其無參構造,建立一個新的post
方式的登陸請求,路徑爲/login
。
進入默認登陸頁
經過HttpServletRequest對象獲取到登陸表單中的用戶名和密碼
建立一個用戶名和密碼的令牌對象
處理登錄表單的信息
(SpringSecurity -- UsernamePasswordAuthenticationFilter.java)
// @since spring security 3.0
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
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;
// 構造器,以不區分大小寫的方式post方式和HTTP方法建立匹配器。
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 從請求路徑獲取用戶名和密碼
String username = obtainUsername(request);
String password = obtainPassword(request);
// 空值判斷
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
// 去除用戶名首尾空格
username = username.trim();
// 生成一個用戶名密碼身份驗證的令牌
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 設置身份認證請求的信息
setDetails(request, authRequest);
// 返回一個徹底通過身份驗證的對象,包括憑據
return this.getAuthenticationManager().authenticate(authRequest);
}
....
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
}
複製代碼
(爲便於解釋,不引入數據庫信息驗證)
實現UserDetailsService
接口,重寫方法。
(service.MyUserDetailsSerice)
/** * 自定義登陸接口(核心接口,加載用戶特定的數據。) */
@Component
public class MyUserDetailsSerice implements UserDetailsService {
// 日誌 返回與做爲參數傳遞的類對應的日誌程序
private static final Logger logger = LoggerFactory.getLogger(UserDetailsService.class);
/** * 校驗,根據用戶名定位用戶 * @param username 標識須要其數據的用戶的用戶名。 * @return 核心用戶信息,一個徹底填充的用戶記錄 * @throws UsernameNotFoundException */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登陸,用戶名:{}", username);
return new User(username, "123", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
複製代碼
繼承WebSecurityConfigurerAdapter
配置類,重寫裏面的配置方法
配置方法可查看官網springboot或查看EnableWebSecurity
接口的註釋信息
(config.MySecurityConfig)
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
// 基礎配置
http.httpBasic()
.and()
// 身份認證
.authorizeRequests()
// 全部請求
.anyRequest()
// 身份認證
.authenticated();
}
複製代碼
返回的User實現了UserDetail接口,詳情見切入源碼
啓動項目,清除瀏覽器緩存,訪問hello,跳轉到默認登陸頁面,校驗密碼。登陸時,用戶名任意,密碼必須爲123(MyUserDetailsSerice中已配置)。
登陸失敗,控制檯打印,沒有針對id「null」PasswordEncoder(映射的密碼編碼器)
繼承PassawordEncoder接口
/** * 用於編碼密碼的服務接口的實現類。 */
@Component
public class MyPasswordEncoder implements PasswordEncoder {
/** * 編碼原始密碼。一般,良好的編碼算法應用SHA-1或更大的哈希與8字節或更大的隨機生成的鹽相結合。 * @param rawPassword 密碼,一個可讀的字符值序列 * @return */
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
/** * 驗證從存儲中得到的編碼密碼是否與提交的原始密碼匹配。若是密碼匹配,返回true;若是不匹配,返回false。存儲的密碼自己永遠不會被解碼。 * @param rawPassword 預設的驗證密碼。要編碼和匹配的原始密碼 * @param encodedPassword 表單輸入的密碼。來自存儲的編碼密碼與之比較 * @return */
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(rawPassword.toString());
}
}
複製代碼
重啓項目,清除瀏覽器緩存,訪問hello。
1 關於WebSecurityConfigurerAdapter
可參考接口EnableWebSecurity
(SpringSecurity -- EnableWebSecurity)
/** * Add this annotation to an {@code @Configuration} class to have the Spring Security * ............. * @Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/public/**").permitAll().anyRequest() * .hasRole("USER").and() * // 更多配置 ... * .formLogin() // 確保基礎表單登陸 * // 爲全部與表單登陸相關聯的URL設置許可證 * .permitAll(); * } * * ................... * @since 3.2 */
...
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
// 默認關閉debug模式
boolean debug() default false;
}
複製代碼
2 security中封裝的默認用戶User的信息 (SpringSecurity -- User.java)
//
public class User implements UserDetails, CredentialsContainer{
...
private String password;
private final String username;
// 用戶權限集合
private final Set<GrantedAuthority> authorities;
// 帳戶未過時
private final boolean accountNonExpired;
// 帳戶未鎖定
private final boolean accountNonLocked;
// 憑據未過時
private final boolean credentialsNonExpired;
// 用戶可用
private final boolean enabled;
...
}
複製代碼
繼承WebSecurityConfigurerAdapter
配置類
在MySecurity中直接注入一個BCryptPasswordEncoder
對象。它實現了PasswordEncoder
接口,並重寫了encode
和matches
方法
(config.MySecurityConfig.java)
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
/** * 實現使用BCrypt強哈希函數的密碼編碼器。客戶機能夠選擇性地提供「強度」(即BCrypt中的日誌輪數)和SecureRandom 實例。 * 強度參數越大,須要作的工做就越多(指數級)來散列密碼。默認值是10。 * @return */
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
...
}
複製代碼
完善MyUserDetailsSerice
(service.MyUserDetailsSerice.java)
@Component
public class MyUserDetailsSerice implements UserDetailsService {
...
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = passwordEncoder.encode("123");
logger.info("登陸,用戶名:{},密碼:{}", username,password);
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
複製代碼
註釋掉MyPasswordEncoder
的@component註解,使其失去容器組件身份
使用debug模式,啓動項目,訪問hello。
debug可看到密碼的轉化,原始密碼123加密爲爲$2aYGYb9i0ZjnTHPlOk/NQb/efrPNOaJq8hJYtdXf8VcdQUi8T8S3Iim
控制檯打印日誌
能夠看到,這裏自動注入的實際上是BCryptPasswordEncoder
對象,並調用了encode方法
(SpringSecurity -- BCryptPasswordEncoder)
// 構造器
public BCryptPasswordEncoder() {
this(-1);
}
public BCryptPasswordEncoder(int strength) {
...
}
public BCryptPasswordEncoder(int strength, SecureRandom random) {
...
}
...
public String encode(CharSequence rawPassword) {
// 鹽值
String salt;
// 判斷構造器是否有相應參數
if (strength > 0) {
if (random != null) {
// 經過random和strength生成的salt
salt = BCrypt.gensalt(strength, random);
}
else {
// 經過strength生成的salt
salt = BCrypt.gensalt(strength);
}
}
// 無參構造
else {
// 調用gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);隨機生成salt
// GENSALT_DEFAULT_LOG2_ROUNDS = 10
salt = BCrypt.gensalt();
}
// 使用OpenBSD bcrypt方案散列密碼,參數分別爲原始密碼和鹽值
return BCrypt.hashpw(rawPassword.toString(), salt);
}
複製代碼
這裏BCryptPasswordEncoder使用的無參,使用默認的鹽值,循環10次,生成了散列的密碼。
這裏雖然是123,但每次加密後都不相同,Spring Security在進行密碼加密的時候,生成了一份隨機salt,最終加密的密碼=密碼+隨機salt。
注意這裏的AuthorityUtils的方法,參數包含角色信息。實際業務中,通常以「ROLE_**」規定用戶的角色字段,並在登陸後授予相應權限
/** *從逗號分隔的字符串表示建立一個GrantedAuthority對象數組(例如「ROLE_A,ROLE_B,ROLE_C」) *@param authorityString 逗號分隔的字符串 *@return 經過標記字符串建立的權限 / AuthorityUtils.commaSeparatedStringToAuthorityList("admin") 複製代碼
不使用springsecurity提供的默認登錄界面
(template.login.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陸</title>
</head>
<body>
<h2>歡迎登陸</h2>
<form action="/auth/login" method="post">
<input name="username" type="text" placeholder="請輸入用戶名.."><br/>
<input name="password" type="password" placeholder="請輸入密碼.."><br/>
<input type="submit" value="登陸">
</form>
</body>
</html>
複製代碼
(template.index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MAIN首頁</title>
</head>
<body>
<h1>歡迎來到首頁</h1>
</body>
</html>
複製代碼
@Controller
public class UserController {
// 登陸測試
...
// 登陸頁,跳轉到/templates/login.html頁面
@GetMapping("/login")
public String login() {
return "login";
}
// 首頁,跳轉到/templates/index.html頁面
@GetMapping("/index")
public String index() {
return "index";
}
}
複製代碼
修改MySecurityConfig中configure方法
(config.MySecurityConfig.java)
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
// 表單認證
.formLogin()
// 登陸頁
.loginPage("/login")
// 登陸表單提交地址
.loginProcessingUrl("/auth/login")
.and()
// 身份認證請求
.authorizeRequests()
// URL路徑匹配
.antMatchers("/login").permitAll()
// 任意請求
.anyRequest()
// 身份認證
.authenticated();
}
}
複製代碼
loginProcessingUrl("/auth/login")
中定義了表單提交地址,但在控制器UserController中並無對應的請求路徑,SpringSecutity默認攔截全部請求,並將URL 302重定向到/login默認登陸頁,使用默認的用戶名密碼便可登陸。
1 自定義登陸成功類
(handler.MyAuthenticationSuccessHandler.java)
/** * 繼承接口,用於處理成功的用戶身份驗證的策略 */
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static final Logger logger = LoggerFactory.getLogger(UserDetailsService.class);
// 提供了讀取和寫入JSON的功能,能夠與基本pojo類進行交互,也能夠與通用JSON樹模型進行交互,還提供了執行轉換的相關功能。
@Autowired
private ObjectMapper objectMapper;
// 當用戶已成功經過身份驗證時調用。
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登陸成功");
response.setContentType("application/json;charset=utf-8");
// writeValueAsString:將java對象序列化爲字符串
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
複製代碼
2 自定義登陸失敗類
(handler.MyAuthenticationFailureHandler.java)
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private static final Logger logger = LoggerFactory.getLogger(UserDetailsService.class);
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("登陸失敗");
// http狀態,200,成功
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
}
}
複製代碼
1 添加登陸成功和失敗的處理方法
(config.MySecurityConfig.java)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/auth/login")
// 登錄成功處理器
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
ObjectMapper om = new ObjectMapper();
String successMsg = om.writeValueAsString(om.writeValueAsString(authentication));
writer.write(successMsg);
writer.flush();
writer.close();
}
})
// 登錄失敗處理器
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write(new ObjectMapper().writeValueAsString(e));
writer.flush();
writer.close();
}
})
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest()
.authenticated();
}
複製代碼
(controller.UserController.java)
@Controller
public class UserController {
...
// 當前用戶信息
@GetMapping("/info")
@ResponseBody
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
}
複製代碼
啓動項目,訪問/info,登陸成功,檢查F12