我們在篇(一)中已經談到了默認的登錄頁面以及默認的登錄賬號和密碼。
在這一篇中我們將自己定義登錄頁面及賬號密碼。
我們先從簡單的開始吧:設置自定義的賬號和密碼(並非從數據庫讀取),雖然意義不大。
上一篇中,我們僅僅重寫了 configure(HttpSecurity http) 方法,該方法是用於完成用戶授權的。
為了完成自定義的認證,我們需要重寫 configure(AuthenticationManagerBuilder auth) 方法。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO Auto-generated method stub auth.inMemoryAuthentication().withUser("Hello").password("{noop}World").roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub http.authorizeRequests() .antMatchers("/login").permitAll() .antMatchers("/user").hasRole("USER") .anyRequest().authenticated() .and() .formLogin().defaultSuccessUrl("/hello"); } }
這個就是新的 WebSecurityConfig 類,控制器里面的方法我就不寫了,仿照(一)很容易寫出來,運行結果你們自己測試吧。
configure(AuthenticationManagerBuilder auth) 方法中,AuthenticationManagerBuilder 的 inMemoryAuthentication() 方法
可以添加用戶,並給用戶指定權限,它還有其他的方法,我們以后用到再講。
在 Password 的地方我們需要注意了:
Spring 5.0 之后為了更加安全,修改了密碼存儲格式,密碼存儲格式為{id}encodedPassword。
id 是一個標識符,用於查找是哪個 PasswordEncoder,也就是密碼加密的格式所對應的 PasswordEncoder。
encodedPassword 是指原始密碼經過加密之后的密碼。id 必須在密碼的開始,id前后必須加 {}。
如果 id 找不到,id 則會為空,會拋出異常:There is no PasswordEncoder mapped for id "null"。
好啦,重點來啦,我們現在開始設置自定義登錄頁面,並從數據庫讀取賬號密碼。
一般來講,我們先講認證原理及流程比較好,不過這個地方我也說不太清楚。那我們還是從例子說起吧。
我用的是 MyBaits 框架操作 Mysql 數據庫。為了支持它們,我們需要在原來的 pom.xml 中添加依賴。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
好啦,現在我們首先定義一個用戶對象以及一個角色對象。
package security.pojo; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; public class SimpleUser implements UserDetails { private static final long serialVersionUID = 1L; private String username; private String password; private String name; private String telephone; private String email; private String headImg; private boolean status = true; private Set<Role> roles; public SimpleUser() { super(); } public SimpleUser(String username, String password, String telephone) { super(); this.username = username; this.password = password; this.telephone = telephone; } public Set<Role> getRoles() { return roles; } public void setRoles(Set<Role> roles) { this.roles = roles; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { // TODO Auto-generated method stub if(!roles.isEmpty()) { List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName())); } return authorities; } return null; } @Override public String getPassword() { // TODO Auto-generated method stub return password; } @Override public String getUsername() { // TODO Auto-generated method stub return username; } @Override public boolean isAccountNonExpired() { // TODO Auto-generated method stub return true; } @Override public boolean isAccountNonLocked() { // TODO Auto-generated method stub return true; } @Override public boolean isCredentialsNonExpired() { // TODO Auto-generated method stub return true; } @Override public boolean isEnabled() { // TODO Auto-generated method stub return status; } public String getTelephone() { return telephone; } public void setTelephone(String telephone) { this.telephone = telephone; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public boolean getStatus() { return status; } public void setStatus(boolean status) { this.status = status; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getHeadImg() { return headImg; } public void setHeadImg(String headImg) { this.headImg = headImg; } }
package security.pojo; public class Role { private String username; private String name; public Role() { super(); } public Role(String username, String name) { super(); this.username = username; this.name = name; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
然后,為了根據用戶名找到用戶,我們定義 Mapper:
package security.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Insert; import security.pojo.SimpleUser; @Mapper public interface SimpleUserMapper { @Select("select * from users where username = #{username}") public SimpleUser findUserByUsername(String username); @Insert("insert into users(username,password,telephone) values(#{username},#{password},#{telephone})") public int addSimpleUser(SimpleUser user); }
package security.mapper; import java.util.Set; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import security.pojo.Role; @Mapper public interface RoleMapper { @Select("select * from roles where username = #{username}") public Set<Role> findRolesByUsername(String username); }
而這樣的一個 Mapper 是不會加載到 Bean 中去的,我們需要對這個類進行掃描:
package security; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("security.mapper") public class SecurityApplication { public static void main(String[] args) { SpringApplication.run(SecurityApplication.class, args); } }
好啦,這個 Mapper 已經成為一個 Bean 了,下面的將是重點:來自 《Spring Boot 2 企業應用實戰》
1、UserDetails
UserDetails 是 Spring Security 的一個核心接口。其中定義了一些可以獲取用戶名、密碼、權限等與認證相關信息的方法。
Spring Security 內部使用的 UserDetails 實現類大都是內置的 User 類,要使用 UserDetails,也可以直接使用該類。
在 Spring Security 內部,很多需要使用用戶信息的時候,基本上都是使用 UserDetails,比如在登錄認證的時候。
UserDetails 是通過 UserDetailsService 的 loadUserByUsername() 方法進行加載的。
我們也需要實現自己的 UserDetailsService 來加載自定義的 UserDetails 信息。
2、UserDetailsService
Authentication.getPrincipal() 的返回類型是 Object,但很多情況下返回的其實是一個 UserDetails 的實例。
登錄認證的時候 Spring Security 會通過 UserDetailsService 的 loadByUsername() 方法獲取相對應的 UserDetails
進行認證,認證通過后會將改 UserDetails 賦給認證通過的 Authentication 的 principal,
然后再把該 Authentication 存入 SecurityContext。之后如果需要使用用戶信息,
可以通過 SecurityContextHolder 獲取存放在 SecurityContext 中的 Authentication 的 principal。
3、Authentication
Authentication 用來表示用戶認證信息,在用戶登錄認證之前,
Spring Security 會將相關信息封裝為一個 Authentication
具體實現類的對象,在登錄認證成功之后又會生成一個信息更全面、包含用戶權限等信息的 Authentication 對象,
然后把它保存在 SpringContextHolder 所持有的 SecurityContext 中,供后續的程序進行調用,如訪問權限的鑒定等。
4、SecurityContextHolder
SecurityContextHolder 是用來保存 SecurityContext 的。SecurityContext 中含有當前所訪問系統的用戶的詳細信息。
默認情況下,SecurityContextHolder 將使用 ThreadLocal 來保存 SecurityContext。
這也就意味着在處於同一線程的方法中,可以從 ThreadLocal 獲取到當前 SecurityContext。
好啦,這個地方就到這兒啦,沒弄懂也不要緊,我們能看懂例子就行了:
package security.service; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; 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.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import security.mapper.RoleMapper; import security.mapper.SimpleUserMapper; import security.pojo.Role; import security.pojo.SimpleUser; @Service public class SimpleUserService implements UserDetailsService { @Autowired private SimpleUserMapper userMapper; @Autowired private RoleMapper roleMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // TODO Auto-generated method stub SimpleUser user = userMapper.findUserByUsername(username); Set<Role> roles = roleMapper.findRolesByUsername(username); if(user == null) { throw new UsernameNotFoundException("Username or Password is not correct"); } user.setRoles(roles); return new User(user.getUsername(),user.getPassword(),user.getAuthorities()); } public int addSimpleUser(SimpleUser user) { user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword())); return userMapper.addSimpleUser(user); } }
在這個類中,我們實現了 UserDetailsService 接口,然后重寫了 loadUserByUsername(String username) 方法。
之后自動注入了一個根據用戶名查找用戶的 Mapper,再將查找的用戶對象復制給 user。
當存在這個用戶的時候,我們獲取它的權限添加到權限列表中,然后把這個列表以及用戶名,密碼存入到 UserDetails 對象中。
因為一個用戶的權限可能不止一個,所以是一個權限列表。
最后我們到了配置環節了:
package security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.crypto.bcrypt.BCryptPasswordEncoder; import security.service.SimpleUserService; // 重寫DaoAuthenticationProvider,authentication 攜帶username,password信息 @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SimpleUserService userService; @Autowired private AuthenticationProvider authenticationProvider; private MessageSource messageSource; protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO Auto-generated method stub auth.authenticationProvider(authenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub http .authorizeRequests() .anyRequest().permitAll() .and() .formLogin().loginPage("/signin") .usernameParameter("username") .passwordParameter("password") .loginProcessingUrl("/signin") .and() .csrf().disable(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new CustomAuthenticationProvider(); provider.setMessageSource(messageSource); provider.setUserDetailsService(userService); provider.setPasswordEncoder(new BCryptPasswordEncoder()); return provider; } }
package security.config; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.util.Assert; public class CustomAuthenticationProvider extends DaoAuthenticationProvider { @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // TODO Auto-generated method stub String presentedPassword = authentication.getCredentials().toString(); if (!getPasswordEncoder().matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "UNameOrPwdIsError","Username or Password is not correct")); } } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // TODO Auto-generated method stub Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); if("".equals(authentication.getPrincipal())) { throw new BadCredentialsException(messages.getMessage( "UsernameIsNull","Username cannot be empty")); } if("".equals(authentication.getCredentials())) { throw new BadCredentialsException(messages.getMessage( "PasswordIsNull","Password cannot be empty")); } String username = (String) authentication.getPrincipal(); boolean cacheWasUsed = true; UserDetails user = this.getUserCache().getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "UNameOrPwdIsError","Username or Password is not correct")); } else { throw notFound; } } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { getPreAuthenticationChecks().check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); getPreAuthenticationChecks().check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } getPostAuthenticationChecks().check(user); if (!cacheWasUsed) { this.getUserCache().putUserInCache(user); } Object principalToReturn = user; if (isForcePrincipalAsString()) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); } }
package security.config; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.ResourceBundleMessageSource; public class MessageSource extends ResourceBundleMessageSource { public MessageSource() { setBasename("messages"); } public static MessageSourceAccessor getAccessor() { return new MessageSourceAccessor(new MessageSource()); } }
在第一個類中我們重寫了兩個 configure() 方法。其中一個我們之前談過,不過並沒有講全,現在補充一下:
在 formLogin() 下還有 .usernameParameter() 和 .passwordParameter() 以及 .loginProcessingUrl("/login") 這三個函數。
前兩個函數是用於指定登錄頁面用戶名及密碼的標識的,后面的一個是用於表單請求的 action 參數。
defaultSuccessUrl 是指定登錄成功顯示的頁面,failureUrl 是指定登錄失敗顯示的頁面。
還有其他的一些我們以后用到再講。
另一個 configure() 方法是用於認證的。我們這里僅僅只寫了一行代碼。
我們把之前的 @Service 的那個類注入到了 userService 中,再把 @Bean 的那個 Bean 注入到了 authenticationProvider 中。
在這個 Bean 里面有個 DaoAuthenticationProvider 類:
Spring Security 默認會使用 DaoAuthenticationProvider 實現 AuthenticationProvider 接口,專門進行用戶認證處理。
DaoAuthenticationProvider 在進行認證處理的時候需要一個 UserDetailsService 來獲取用戶的信息 UserDetails,
其中包括用戶名,密碼和所擁有的權限等。
看到這些代碼,可以知道我們寫的代碼都有聯系了。我們還差一個控制器的代碼:
package security.controller; import java.util.Random; import security.pojo.SimpleUser; import security.service.SimpleUserService; import com.zhenzi.sms.ZhenziSmsClient; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller public class SecurityController { @Autowired private SimpleUserService userService; @GetMapping("/signin") public String signIn() { return "signin"; } @GetMapping("/signup") public String signUp() { return "signup"; } @PostMapping("/sign_up") public String regist(@RequestParam(value="verifycode") String code,HttpServletRequest request, SimpleUser user) { String verifycode = (String) request.getSession().getAttribute("verifyCode"); if(!code.equals(verifycode)){ return "failure"; } userService.addSimpleUser(user); return "signin"; } @PostMapping("/sendsms")// 若不要 response 參數,則會發出 /sendsms 請求。 public void sendsms(HttpServletRequest request, HttpServletResponse response, String telephone) { try { String verifyCode = String.valueOf(new Random().nextInt(899999) + 100000); ZhenziSmsClient client = new ZhenziSmsClient("******", "******", "******"); client.send(telephone, "您的驗證碼為 " + verifyCode + ",有效期為 3 分鍾,如非本人操作,可不予理會!"); request.getSession().setAttribute("verifyCode", verifyCode); } catch (Exception e) { e.printStackTrace(); } } }
好啦,到此 java 代碼就結束了。
前面我們設置了 usernameParameter("username"),passwordParameter("password"),
另外由於默認的登錄頁面表單請求的 action="/login",用戶名參數和密碼分別為 "username","password"。
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 = "username"; private String passwordParameter = "password"; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } // ... ... }
如果用 thymeleaf 模板的話,這三個參數就分別用 th:action="{/login}" ,th:name="username",th:name="password"。
若是我們想自定義的話,比如登錄頁面為 signin.html,登錄請求的 action 為 "/signin",
用戶名參數為 uname,密碼參數為 pwd。
@Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub http.authorizeRequests() .antMatchers("/css/**","/images/*","/js/**","/login").permitAll() .antMatchers("/index").hasRole("USER") .anyRequest().authenticated() .and() .formLogin().loginPage("/login") .usernameParameter("uname") .passwordParameter("pwd") .loginProcessingUrl("/sign") .defaultSuccessUrl("/success") .failureUrl("/failure"); }
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <form th:action="@{/signin}" method="post"> <input th:name="uname" type="text"> <input th:name="pwd" type="password"> <input type="submit" value="login"> </form> </body> </html>
熬,對啦,連接數據庫的地方需要寫在 application.properties 文件里:
注意了,那個 url 數據庫(security)后面一定要寫上 ?serverTimezone=UTC&characterEncoding=utf-8 這樣的,不然會出錯的。
至此,入門項目就結束了,所有的源碼都在上面啦,覺得可以的話點個贊啦!
鏈接:https://pan.baidu.com/s/13fc6P9NV49aRRBctr3MjNQ
提取碼:4qgu