Spring Security使用詳解(基本用法 )
1,什么是 Spring Security ?
- Spring Security 是一個相對復雜的安全管理框架,功能比 Shiro 更加強大,權限控制細粒度更高,對 OAuth 2 的支持也更友好。
- 由於 Spring Security 源自 Spring 家族,因此可以和 Spring 框架無縫整合,特別是 Spring Boot 中提供的自動化配置方案,可以讓 Spring Security 的使用更加便捷。
2,安裝配置
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
3、開始測試:
首先在項目添加一個簡單的 /hello 接口:
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "歡迎訪問 hangge.com"; } }
接着啟動項目,直接訪問 /hello 接口則會自動跳轉到登錄頁面(這個登錄頁面是由 Spring Security 提供的)
(3)我們必須登錄后才能訪問 /hello 接口。默認用戶名是 user,而登錄密碼則在每次啟動項目時隨機生成,我們可以在項目啟動日志中找到。
(4)登錄后則會自動跳轉到之前我訪問的 /hello 接口:
4,配置用戶名和密碼
spring.security.user.name=hangge spring.security.user.password=123 spring.security.user.roles=admin
基於內存的用戶、URL權限配置:
1,用戶角色配置:
(1)我們可以通過自定義類繼承 WebSecurityConfigurerAdapter,從而實現對 Spring Security 更多的自定義配置。比如下面樣例我們就配置了兩個用戶,以及他們對應的角色(這種方式只適合用於測試、開發環境不適用於生產)
@Configuration public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
// 指定密碼的加密方式
@Bean
public PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return Objects.equals(charSequence.toString(), s);
}
};
}
// 配置用戶及其對應的角色 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("root").password("123").roles("ADMIN","DBA") .and() .withUser("admin").password("123").roles("ADMIN","USER") .and() .withUser("hangge").password("123").roles("USER"); }
// 配置 URL 訪問權限
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 開啟 HttpSecurity 配置 .antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL必須具備ADMIN角色 .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") // 該模式需要ADMIN或USER角色 .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 需ADMIN和DBA角色 .anyRequest().authenticated() // 用戶訪問其它URL都必須認證后訪問(登錄后訪問) .and().formLogin().loginProcessingUrl("/login").permitAll() // 開啟表單登錄並配置登錄接口 .and().csrf().disable(); // 關閉csrf }
}
(2)配置完成后,重啟項目,就可以使用這兩個用戶進行登錄了。
- formLogin() 方法表示開啟表單登錄,即我們之前看到的登錄頁面。
- loginProcessingUrl() 方法配置登錄接口為“/login”,即可以直接調用“/login”接口,發起一個 POST 請求進行登錄,登錄參數中用戶名必須為 username,密碼必須為 password,配置 loginProcessingUrl 接口主要是方便 Ajax 或者移動端調用登錄接口。
- permitAll() 表示和登錄相關的接口都不需要認證即可訪問。
三、基於數據庫的用戶角色配置
maven依賴:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--操作數據庫--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MySQL 驅動 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- Druid 數據庫連接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.22</version> </dependency> <!-- region MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <!--模板引擎thmeleaf對HTML的支持--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </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>
2,創建數據表:
CREATE TABLE `resources` ( `id` int(11) NOT NULL AUTO_INCREMENT, `pattern` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL, `description` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; CREATE TABLE `role_resource` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_id` int(11) DEFAULT NULL, `resource_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; CREATE TABLE `user` ( `id` int(64) NOT NULL AUTO_INCREMENT, `user_name` varchar(32) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `enable` tinyint(4) DEFAULT NULL, `locked` tinyint(4) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `role_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
3,創建實體類
public class Resources { private Integer id; private String pattern; private List<Role> roles; }
public class Role implements Serializable { private static final long serialVersionUID = 825384782616737527L; private Integer id; private String name; private String description; }
public class User implements UserDetails { private Integer id; private String userName; private String password; private boolean enable; private boolean locked; private List<Role> userRoles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (Role role : userRoles) { authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; } @Override public String getUsername() { return userName; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enable; } public void setPassword(String password) { this.password = password; } public String getPassword() { return password; } public boolean isEnable() { return enable; } public void setEnable(boolean enable) { this.enable = enable; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public boolean isLocked() { return locked; } public void setLocked(boolean locked) { this.locked = locked; } public List<Role> getUserRoles() { return userRoles; } public void setUserRoles(List<Role> userRoles) { this.userRoles = userRoles; } }
接着創建用戶表對應的實體類。用戶實體類需要實現 UserDetails 接口,並實現該接口中的 7 個方法:
- getAuthorities():獲取當前用戶對象所具有的角色信息
- getPassword():獲取當前用戶對象的密碼
- getUsername():獲取當前用戶對象的用戶名
- isAccountNonExpired():當前賬戶是否未過期
- isAccountNonLocked():當前賬戶是否未鎖定
- isCredentialsNonExpired():當前賬戶密碼是否未過期
- isEnabled():當前賬戶是否可用
(1)用戶根據實際情況設置這 7 個方法的返回值。默認情況下不需要開發者自己進行密碼角色等信息的比對,開發者只需要提供相關信息即可,例如:
- getPassword() 方法返回的密碼和用戶輸入的登錄密碼不匹配,會自動拋出 BadCredentialsException 異常
- isAccountNonLocked() 方法返回了 false,會自動拋出 AccountExpiredException 異常。
- 本案例因為數據庫中只有 enabled 和 locked 字段,故賬戶未過期和密碼未過期兩個方法都返回 true.
(2)getAuthorities 方法用來獲取當前用戶所具有的角色信息,本案例中,用戶所具有的角色存儲在 roles 屬性中,因此該方法直接遍歷 roles屬性,然后構造 SimpleGrantedAuthority 集合並返回。
4,創建數據庫訪問層
(1)首先創建 UserMapper 接口:
@Repository public interface UserMapperDao { public User loadUserByUsername(String userName); public List<Role> getUserRolesByUid(Integer id); }
(2)接着在 UserMapper 相同的位置創建 UserMapper.xml 文件,內容如下:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="example.hellosecurity.dao.UserMapperDao"> <select id="loadUserByUsername" parameterType="string" resultType="example.hellosecurity.entity.User"> select * from user where user_name = #{userName} </select> <select id="getUserRolesByUid" parameterType="int" resultType="example.hellosecurity.entity.Role"> select * from role r, user_role ur where r.id = ur.role_id and ur.user_id = #{id} </select> </mapper>
5,創建 UserService
定義的 UserService 實現 UserDetailsService 接口,並實現該接口中的 loadUserByUsername 方法,該方法將在用戶登錄時自動調用。
loadUserByUsername 方法的參數就是用戶登錄時輸入的用戶名,通過用戶名去數據庫中查找用戶:
- 如果沒有查找到用戶,就拋出一個賬戶不存在的異常。
- 如果查找到了用戶,就繼續查找該用戶所具有的角色信息,並將獲取到的 user 對象返回,再由系統提供的 DaoAuthenticationProvider類去比對密碼是否正確。
@Service public class UserService implements UserDetailsService { @Autowired private UserMapperDao userMapperDao; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapperDao.loadUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("賬戶不存在!"); } // 我的數據庫用戶密碼沒加密,這里手動設置 String encodePassword = passwordEncoder.encode(user.getPassword()); System.out.println("加密后的密碼:" + encodePassword); user.setPassword(encodePassword); List<Role> userRoles = userMapperDao.getUserRolesByUid(user.getId()); user.setUserRoles(userRoles); return user; } }
6,配置 Spring Security
Spring Security 大部分配置與前文一樣,只不過這次沒有配置內存用戶,而是將剛剛創建好的 UserService 配置到 AuthenticationManagerBuilder 中。
@Configuration public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; // 指定密碼的加密方式 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new PasswordEncoder() { // @Override // public String encode(CharSequence charSequence) { // return charSequence.toString(); // } // // @Override // public boolean matches(CharSequence charSequence, String s) { // return Objects.equals(charSequence.toString(), s); // } // }; } // 配置用戶及其對應的角色 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } // 配置基於內存的 URL 訪問權限 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 開啟 HttpSecurity 配置 .antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL必須具備ADMIN角色 .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") // 該模式需要ADMIN或USER角色 .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 需ADMIN和DBA角色 .anyRequest().authenticated() // 用戶訪問其它URL都必須認證后訪問(登錄后訪問) .and().formLogin().loginProcessingUrl("/login").permitAll() // 開啟表單登錄並配置登錄接口 .and().csrf().disable(); // 關閉csrf }
7,運行測試
1)首先在 Conctoller 中添加如下接口進行測試:
@RestController public class HelloController { @GetMapping("/admin/hello") public String admin() { return "hello admin"; } @GetMapping("/user/hello") public String user() { return "hello user"; } @GetMapping("/db/hello") public String db() { return "hello db"; } @GetMapping("/hello") public String hello() { return "hello"; } }
接下來測試一下,我們使用 admin 用戶進行登錄,由於該用戶具有 ADMIN 和 USER 這兩個角色,所以登錄后可以訪問 /hello、/admin/hello 以及 /user/hello 這三個接口。
雖然前面我們實現了通過數據庫來配置用戶與角色,但認證規則仍然是使用 HttpSecurity 進行配置,還是不夠靈活,無法實現資源和角色之間的動態調整。
四、基於數據庫的URL權限規則配置
下面是基於 resource 表 和 role_resource 表來實現:
(1)首先創建 resourceMapper 接口:
@Repository public interface ResourceMapperDao { /** * @Author dw * @Description 獲取所有的資源 * @Date 2020/4/15 11:16 * @Param * @return */ public List<Resources> getAllResources(); }
xml:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="example.hellosecurity.dao.ResourceMapperDao"> <resultMap id="ResourcesMap" type="example.hellosecurity.entity.Resources"> <id column="id" property="id"/> <result column="pattern" property="pattern"/> <collection property="roles" ofType="example.hellosecurity.entity.Role"> <id column="roleId" property="id"/> <result column="name" property="name"/> <result column="description" property="description"/> </collection> </resultMap> <select id="getAllResources" resultMap="ResourcesMap"> SELECT r.*, re.id AS roleId, re.`name`, re.description FROM resources AS r LEFT JOIN role_resource AS rr ON r.id = rr.resource_id LEFT JOIN role AS re ON re.id = rr.role_id </select> </mapper>
自定義 FilterInvocationSecurityMetadataSource
注意:自定義 FilterInvocationSecurityMetadataSource 主要實現該接口中的 getAttributes 方法,該方法用來確定一個請求需要哪些角色。
/** * @Author dw * @ClassName CustomFilterInvocationSecurityMetadataSource * @Description 要實現動態配置權限,首先需要自定義 FilterInvocationSecurityMetadataSource: * 自定義 FilterInvocationSecurityMetadataSource 主要實現該接口中的 getAttributes 方法,該方法用來確定一個請求需要哪些角色。 * @Date 2020/4/15 11:36 * @Version 1.0 */ @Component public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { // 創建一個AntPathMatcher,主要用來實現ant風格的URL匹配。 AntPathMatcher antPathMatcher = new AntPathMatcher(); @Autowired private ResourceMapperDao resourceMapperDao; @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { // 從參數中提取出當前請求的URL String requestUrl = ((FilterInvocation) object).getRequestUrl(); // 從數據庫中獲取所有的資源信息,即本案例中的Resources表以及Resources所對應的role // 在真實項目環境中,開發者可以將資源信息緩存在Redis或者其他緩存數據庫中。 List<Resources> allResources = resourceMapperDao.getAllResources(); // 遍歷資源信息,遍歷過程中獲取當前請求的URL所需要的角色信息並返回。 for (Resources resource : allResources) { if (antPathMatcher.match(resource.getPattern(), requestUrl)) { List<Role> roles = resource.getRoles(); if(!CollectionUtils.isEmpty(roles)){ List<ConfigAttribute> allRoleNames = roles.stream() .map(role -> new SecurityConfig(role.getName().trim())) .collect(Collectors.toList()); return allRoleNames; } } } // 如果當前請求的URL在資源表中不存在相應的模式,就假設該請求登錄后即可訪問,即直接返回 ROLE_LOGIN. return SecurityConfig.createList("ROLE_LOGIN"); } // 該方法用來返回所有定義好的權限資源,Spring Security在啟動時會校驗相關配置是否正確。 @Override public Collection<ConfigAttribute> getAllConfigAttributes() { // 如果不需要校驗,那么該方法直接返回null即可。 return null; } // supports方法返回類對象是否支持校驗。 @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
自定義 AccessDecisionManager
當一個請求走完 FilterInvocationSecurityMetadataSource 中的 getAttributes 方法后,接下來就會來到 AccessDecisionManager 類中進行角色信息的對比,自定義 AccessDecisionManager 代碼如下:
@Component public class CustomAccessDecisionManager implements AccessDecisionManager { // 該方法判斷當前登錄的用戶是否具備當前請求URL所需要的角色信息 @Override public void decide(Authentication auth, Object object, Collection<ConfigAttribute> ConfigAttributes){ Collection<? extends GrantedAuthority> userHasAuthentications = auth.getAuthorities(); // 如果具備權限,則不做任何事情即可 for (ConfigAttribute configAttribute : ConfigAttributes) { // 如果需要的角色是ROLE_LOGIN,說明當前請求的URL用戶登錄后即可訪問 // 如果auth是UsernamePasswordAuthenticationToken的實例,說明當前用戶已登錄,該方法到此結束 if ("ROLE_LOGIN".equals(configAttribute.getAttribute()) && auth instanceof UsernamePasswordAuthenticationToken) { return; } // 否則進入正常的判斷流程 for (GrantedAuthority authority : userHasAuthentications) { // 如果當前用戶具備當前請求需要的角色,那么方法結束。 if (configAttribute.getAttribute().equals(authority.getAuthority())) { return; } } } // 如果不具備權限,就拋出AccessDeniedException異常 throw new AccessDeniedException("權限不足"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
配置 Spring Security
這里與前文的配置相比,主要是修改了 configure(HttpSecurity http) 方法的實現並添加了兩個 Bean。至此我們邊實現了動態權限配置,權限和資源的關系可以在 role_resource表中動態調整。
修改 MyWebSecurityConfig:
// 配置基於數據庫的 URL 訪問權限 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(accessMustRoles()); object.setAccessDecisionManager(rolesCheck()); return object; } }) .and().formLogin().loginProcessingUrl("/login").permitAll()//開啟表單登錄並配置登錄接口 .and().csrf().disable(); // 關閉csrf } @Bean public CustomFilterInvocationSecurityMetadataSource accessMustRoles() { return new CustomFilterInvocationSecurityMetadataSource(); } @Bean public CustomAccessDecisionManager rolesCheck() { return new CustomAccessDecisionManager(); }
要配置角色繼承關系,只需在 Spring Security 的配置類中提供一個 RoleHierarchy 即可。
// 配置角色繼承關系 @Bean RoleHierarchy roleHierarchy() { RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); String hierarchy = "ROLE_DBA > ROLE_ADMIN > ROLE_USER"; roleHierarchy.setHierarchy(hierarchy); return roleHierarchy; }
在之前的所有樣例中,登錄表單一直都是使用 Spring Security 提供的默認登錄頁,登錄成功后也是默認的頁面跳轉。有時我們想要使用自定義的登錄頁,或者在前后端分離的開發方式中,前后端的數據交互通過 JSON 進行,這時登錄成功后就不是頁面跳轉了,而是一段 JSON 提示。下面通過樣例演示如何進行登錄表單的個性化配置。
自定義登錄頁面、登錄接口、登錄成功或失敗的處理邏輯
- 將登錄頁改成使用自定義頁面,並配置登錄請求處理接口,以及用戶密碼提交時使用的參數名。
- 自定義了登錄成功、登錄失敗的處理邏輯,根據情況返回響應的 JSON 數據。
@Configuration public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { // 指定密碼的加密方式 @SuppressWarnings("deprecation") @Bean PasswordEncoder passwordEncoder(){ // 不對密碼進行加密 return NoOpPasswordEncoder.getInstance(); } // 配置用戶及其對應的角色 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("root").password("123").roles("DBA") .and() .withUser("admin").password("123").roles("ADMIN") .and() .withUser("hangge").password("123").roles("USER"); } // 配置 URL 訪問權限 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 開啟 HttpSecurity 配置 .antMatchers("/db/**").hasRole("DBA") // db/** 模式URL需DBA角色 .antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL需ADMIN角色 .antMatchers("/user/**").hasRole("USER") // user/** 模式URL需USER角色 .anyRequest().authenticated() // 用戶訪問其它URL都必須認證后訪問(登錄后訪問) .and().formLogin() // 開啟登錄表單功能 .loginPage("/login_page") // 使用自定義的登錄頁面,不再使用SpringSecurity提供的默認登錄頁 .loginProcessingUrl("/login") // 配置登錄請求處理接口,自定義登錄頁面、移動端登錄都使用該接口 .usernameParameter("name") // 修改認證所需的用戶名的參數名(默認為username) .passwordParameter("passwd") // 修改認證所需的密碼的參數名(默認為password) // 定義登錄成功的處理邏輯(可以跳轉到某一個頁面,也可以返會一段 JSON) .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException, ServletException { // 我們可以跳轉到指定頁面 // resp.sendRedirect("/index"); // 也可以返回一段JSON提示 // 獲取當前登錄用戶的信息,在登錄成功后,將當前登錄用戶的信息一起返回給客戶端 Object principal = auth.getPrincipal(); resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); resp.setStatus(200); Map<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("msg", principal); ObjectMapper om = new ObjectMapper(); out.write(om.writeValueAsString(map)); out.flush(); out.close(); } }) // 定義登錄失敗的處理邏輯(可以跳轉到某一個頁面,也可以返會一段 JSON) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); resp.setStatus(401); Map<String, Object> map = new HashMap<>(); // 通過異常參數可以獲取登錄失敗的原因,進而給用戶一個明確的提示。 map.put("status", 401); if (e instanceof LockedException) { map.put("msg", "賬戶被鎖定,登錄失敗!"); }else if(e instanceof BadCredentialsException){ map.put("msg","賬戶名或密碼輸入錯誤,登錄失敗!"); }else if(e instanceof DisabledException){ map.put("msg","賬戶被禁用,登錄失敗!"); }else if(e instanceof AccountExpiredException){ map.put("msg","賬戶已過期,登錄失敗!"); }else if(e instanceof CredentialsExpiredException){ map.put("msg","密碼已過期,登錄失敗!"); }else{ map.put("msg","登錄失敗!"); } ObjectMapper mapper = new ObjectMapper(); out.write(mapper.writeValueAsString(map)); out.flush(); out.close(); } }) .permitAll() // 允許訪問登錄表單、登錄接口 .and().csrf().disable(); // 關閉csrf } }
(2)在 resource/templates 目錄下創建一個登錄頁面 login_page.html,內容如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login" method="post"> <div> <label>用戶名</label> <input type="text" name="name"/> </div> <div> <label>密碼</label> <input type="password" name="passwd"/> </div> <div> <input type="submit" value="登陸"> </div> </form> </body> </html>
七、注銷登錄配置
修改 Spring Security 配置
// 配置 URL 訪問權限 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 開啟 HttpSecurity 配置 .antMatchers("/db/**").hasRole("DBA") // db/** 模式URL需DBA角色 .antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL需ADMIN角色 .antMatchers("/user/**").hasRole("USER") // user/** 模式URL需USER角色 .anyRequest().authenticated() // 用戶訪問其它URL都必須認證后訪問(登錄后訪問) .and().formLogin().loginProcessingUrl("/login").permitAll() // 開啟表單登錄並配置登錄接口 .and().logout() // 開啟注銷登錄的配置 .logoutUrl("/logout") // 配置注銷登錄請求URL為"/logout"(默認也就是 /logout) .clearAuthentication(true) // 清除身份認證信息 .invalidateHttpSession(true) // 使 session 失效 // 配置一個 LogoutHandler,開發者可以在這里完成一些數據清除工做 .addLogoutHandler(new LogoutHandler() { @Override public void logout(HttpServletRequest req, HttpServletResponse resp, Authentication auth) { System.out.println("注銷登錄,開始清除Cookie。"); } }) // 配置一個 LogoutSuccessHandler,開發者可以在這里處理注銷成功后的業務邏輯 .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException, ServletException { // 我們可以跳轉到登錄頁面 // resp.sendRedirect("/login"); // 也可以返回一段JSON提示 resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); resp.setStatus(200); Map<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("msg", "注銷成功!"); ObjectMapper om = new ObjectMapper(); out.write(om.writeValueAsString(map)); out.flush(); out.close(); } }) .and().csrf().disable(); // 關閉csrf }
密碼加密配置
(1)要配置密碼加密只需要修改兩個地方。首先要修改 HttpSecurity 配置中的 PasswordEncoder 這個Bean 的實現,這里我們采用 BCryptPasswordEncoder 加密方案。
Spring Security 提供了多種密碼加密方案,官方推薦使用 BCryptPasswordEncoder: BCryptPasswordEncoder 使用 BCrypt 強哈希函數,開發者在使用時可以選擇提供 strength 和 SecureRandom 實例。 strength 取值在 4~31 之間(默認為 10)。strength 越大,密鑰的迭代次數越多(密鑰迭代次數為 2^strength)
(2)接着將用戶的密碼改成使用 BCryptPasswordEncoder 加密后的密碼(如果是數據庫認證,庫里的密碼同樣也存放加密后的密碼)
@Bean PasswordEncoder passwordEncoder(){ // 使用BCrypt強哈希函數加密方案,密鑰迭代次數設為10(默認即為10) return new BCryptPasswordEncoder(10); }
通過注解配置方法安全
1)首先我們要通過 @EnableGlobalMethodSecurity 注解開啟基於注解的安全配置:
@EnableGlobalMethodSecurity 注解參數說明:
- prePostEnabled = true 會解鎖 @PreAuthorize 和 @PostAuthorize 兩個注解。顧名思義,@PreAuthorize 注解會在方法執行前進行驗證,而 @PostAuthorize 注解會在方法執行后進行驗證。
- securedEnabled = true 會解鎖 @Secured 注解。
(2)開啟注解安全配置后,接着創建一個 MethodService 進行測試:
@Service public class MethodService { // 訪問該方法需要 ADMIN 角色。注意:這里需要在角色前加一個前綴"ROLE_" @Secured("ROLE_ADMIN") public String admin() { return "hello admin"; } // 訪問該方法既要 ADMIN 角色,又要 DBA 角色 @PreAuthorize("hasRole('ADMIN') and hasRole('DBA')") public String dba() { return "hello dba"; } // 訪問該方法只需要 ADMIN、DBA、USER 中任意一個角色即可 @PreAuthorize("hasAnyRole('ADMIN','DBA','USER')") public String user() { return "hello user"; } }
獲取用戶信息:
(1)通過 Authentication.getPrincipal() 可以獲取到代表當前用戶的信息,這個對象通常是 UserDetails 的實例。通過 UserDetails 的實例我們可以獲取到當前用戶的用戶名、密碼、角色等信息。
Spring Security 使用一個 Authentication 對象來描述當前用戶的相關信息,而 SecurityContext 持有的是代表當前用戶相關信息的 Authentication 的引用。
這個 Authentication 對象不需要我們自己去創建,在與系統交互的過程中,Spring Security 會自動為我們創建相應的 Authentication 對象,然后賦值給當前的 SecurityContext。
方式一:
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "當前登錄用戶:" + SecurityContextHolder.getContext().getAuthentication().getName(); } }
方式2:
/** * 獲取用戶明細 * @param principal * @return */ @RequestMapping(value = "getUserInfo", method = RequestMethod.GET) public Principal getUserDetails(Principal principal) { logger.info("用戶名:{}",principal.getName()); return principal; } /** * 獲取用戶明細 * @param authentication * @return */ @RequestMapping(value = "getUserInfo2", method = RequestMethod.GET) public Authentication getUserInfo2(Authentication authentication) { logger.info("用戶名:{}", authentication); return authentication; } /** * 只獲取用戶信息 * @param userDetails * @return */ @RequestMapping(value = "getUser", method = RequestMethod.GET) public UserDetails getUser(@AuthenticationPrincipal UserDetails userDetails) { logger.info("用戶名:{}",userDetails.getUsername()); return userDetails; }