Spring Security,這是一種基於 Spring AOP 和 Servlet 過濾器的安全框架。它提供全面的安全性解決方案,同時在 Web 請求級和方法調用級處理身份確認和授權。
從網上找了一張Spring Security 的工作流程圖,如下。
圖中標記的MyXXX,就是我們項目中需要配置的。
表結構
建表語句
DROP TABLE IF EXISTS `user`; DROP TABLE IF EXISTS `role`; DROP TABLE IF EXISTS `user_role`; DROP TABLE IF EXISTS `role_permission`; DROP TABLE IF EXISTS `permission`;
CREATE TABLE `user` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `role` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `user_role` ( `user_id` bigint(11) NOT NULL, `role_id` bigint(11) NOT NULL ); CREATE TABLE `role_permission` ( `role_id` bigint(11) NOT NULL, `permission_id` bigint(11) NOT NULL ); CREATE TABLE `permission` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `url` varchar(255) NOT NULL, `name` varchar(255) NOT NULL, `description` varchar(255) NULL, `pid` bigint(11) NOT NULL, PRIMARY KEY (`id`) );
INSERT INTO user (id, username, password) VALUES (1,user,e10adc3949ba59abbe56e057f20f883e); INSERT INTO user (id, username , password) VALUES (2,admin,e10adc3949ba59abbe56e057f20f883e); INSERT INTO role (id, name) VALUES (1,USER); INSERT INTO role (id, name) VALUES (2,ADMIN); INSERT INTO permission (id, url, name, pid) VALUES (1,/user/common,common,0); INSERT INTO permission (id, url, name, pid) VALUES (2,/user/admin,admin,0); INSERT INTO user_role (user_id, role_id) VALUES (1, 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 2); INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1); INSERT INTO role_permission (role_id, permission_id) VALUES (2, 1); INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-security4</artifactId> </dependency>
spring: thymeleaf: mode: HTML5 encoding: UTF-8 cache: false
datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/spring-security?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root
public class User implements UserDetails , Serializable {
private Long id; private String username; private String password;
private List<Role> authorities;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
@Override public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
@Override public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
@Override public List<Role> getAuthorities() { return authorities; }
public void setAuthorities(List<Role> authorities) { this.authorities = authorities; }
/** * 用戶賬號是否過期 */ @Override public boolean isAccountNonExpired() { return true; }
/** * 用戶賬號是否被鎖定 */ @Override public boolean isAccountNonLocked() { return true; }
/** * 用戶密碼是否過期 */ @Override public boolean isCredentialsNonExpired() { return true; }
/** * 用戶是否可用 */ @Override public boolean isEnabled() { return true; }
}
上面的 User 類實現了 UserDetails 介面,該介面是實現Spring Security 認證信息的核心介面。其中 getUsername 方法為 UserDetails 介面 的方法,這個方法返回 username,也可以是其他的用戶信息,例如手機號、郵箱等。getAuthorities() 方法返回的是該用戶設置的許可權信息,在本實例中,從資料庫取出用戶的所有角色信息,許可權信息也可以是用戶的其他信息,不一定是角色信息。另外需要讀取密碼,最後幾個方法一般情況下都返回 true,也可以根據自己的需求進行業務判斷。
public class Role implements GrantedAuthority {
private Long id; private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@Override public String getAuthority() { return name; }
Role 類實現了 GrantedAuthority 介面,並重寫 getAuthority() 方法。許可權點可以為任何字元串,不一定非要用角色名。
所有的Authentication實現類都保存了一個GrantedAuthority列表,其表示用戶所具有的許可權。GrantedAuthority是通過AuthenticationManager設置到Authentication對象中的,然後AccessDecisionManager將從Authentication中獲取用戶所具有的GrantedAuthority來鑒定用戶是否具有訪問對應資源的許可權。
@Service public class MyUserDetailsService implements UserDetailsService {
@Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper;
@Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { //查資料庫 User user = userMapper.loadUserByUsername( userName ); if (null != user) { List<Role> roles = roleMapper.getRolesByUserId( user.getId() ); user.setAuthorities( roles ); }
return user; }
Service 層需要實現 UserDetailsService 介面,該介面是根據用戶名獲取該用戶的所有信息, 包括用戶信息和許可權點。
@Component public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Autowired private PermissionMapper permissionMapper;
/** * 每一個資源所需要的角色 Collection<ConfigAttribute>決策器會用到 */ private static HashMap<String, Collection<ConfigAttribute>> map =null;
/** * 返回請求的資源需要的角色 */ @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { if (null == map) { loadResourceDefine(); } //object 中包含用戶請求的request 信息 HttpServletRequest request = ((FilterInvocation) o).getHttpRequest(); for (Iterator<String> it = map.keySet().iterator() ; it.hasNext();) { String url = it.next(); if (new AntPathRequestMatcher( url ).matches( request )) { return map.get( url ); } }
return null; }
@Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; }
@Override public boolean supports(Class<?> aClass) { return true; }
/** * 初始化 所有資源 對應的角色 */ public void loadResourceDefine() { map = new HashMap<>(16); //許可權資源 和 角色對應的表 也就是 角色許可權 中間表 List<RolePermisson> rolePermissons = permissionMapper.getRolePermissions();
//某個資源 可以被哪些角色訪問 for (RolePermisson rolePermisson : rolePermissons) {
String url = rolePermisson.getUrl(); String roleName = rolePermisson.getRoleName(); ConfigAttribute role = new SecurityConfig(roleName);
if(map.containsKey(url)){ map.get(url).add(role); }else{ List<ConfigAttribute> list = new ArrayList<>(); list.add( role ); map.put( url , list ); } } }
MyInvocationSecurityMetadataSourceService 類實現了 FilterInvocationSecurityMetadataSource,FilterInvocationSecurityMetadataSource 的作用是用來儲存請求與許可權的對應關係。
FilterInvocationSecurityMetadataSource介面有3個方法:
/** * 決策器 */ @Component public class MyAccessDecisionManager implements AccessDecisionManager {
private final static Logger logger = LoggerFactory.getLogger(MyAccessDecisionManager.class);
/** * 通過傳遞的參數來決定用戶是否有訪問對應受保護對象的許可權 * * @param authentication 包含了當前的用戶信息,包括擁有的許可權。這裡的許可權來源就是前面登錄時UserDetailsService中設置的authorities。 * @param object 就是FilterInvocation對象,可以得到request等web資源 * @param configAttributes configAttributes是本次訪問需要的許可權 */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if (null == configAttributes || 0 >= configAttributes.size()) { return; } else { String needRole; for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) { needRole = iter.next().getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) { if(needRole.trim().equals(ga.getAuthority().trim())) { return; } } } throw new AccessDeniedException("當前訪問沒有許可權"); }
/** * 表示此AccessDecisionManager是否能夠處理傳遞的ConfigAttribute呈現的授權請求 */ @Override public boolean supports(ConfigAttribute configAttribute) { return true; }
/** * 表示當前AccessDecisionManager實現是否能夠為指定的安全對象(方法調用或Web請求)提供訪問控制決策 */ @Override public boolean supports(Class<?> aClass) { return true; }
MyAccessDecisionManager 類實現了AccessDecisionManager介面,AccessDecisionManager是由AbstractSecurityInterceptor調用的,它負責鑒定用戶是否有訪問對應資源(方法或URL)的許可權。
@Component public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) { super.setAccessDecisionManager(myAccessDecisionManager); }
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain); invoke(fi); }
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi); try { //執行下一個攔截器 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } }
@Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; }
@Override public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource; }
每種受支持的安全對象類型(方法調用或Web請求)都有自己的攔截器類,它是AbstractSecurityInterceptor的子類,AbstractSecurityInterceptor 是一個實現了對受保護對象的訪問進行攔截的抽象類。
AbstractSecurityInterceptor的機制可以分為幾個步驟:
AbstractSecurityInterceptor中的方法說明:
了解了AbstractSecurityInterceptor,就應該明白了,我們自定義MyFilterSecurityInterceptor就是想使用我們之前自定義的 AccessDecisionManager 和 securityMetadataSource。
@EnableWebSecurity註解以及WebSecurityConfigurerAdapter一起配合提供基於web的security。自定義類 繼承了WebSecurityConfigurerAdapter來重寫了一些方法來指定一些特定的Web安全設置。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private MyUserDetailsService userService;
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//校驗用戶 auth.userDetailsService( userService ).passwordEncoder( new PasswordEncoder() { //對密碼進行加密 @Override public String encode(CharSequence charSequence) { System.out.println(charSequence.toString()); return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()); } //對密碼進行判斷匹配 @Override public boolean matches(CharSequence charSequence, String s) { String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()); boolean res = s.equals( encode ); return res; } } );
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/","index","/login","/login-error","/401","/css/**","/js/**").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage( "/login" ).failureUrl( "/login-error" ) .and() .exceptionHandling().accessDeniedPage( "/401" ); http.logout().logoutSuccessUrl( "/" ); }
@Controller public class MainController {
@RequestMapping("/") public String root() { return "redirect:/index"; }
@RequestMapping("/index") public String index() { return "index"; }
@RequestMapping("/login") public String login() { return "login"; }
@RequestMapping("/login-error") public String loginError(Model model) { model.addAttribute( "loginError" , true); return "login"; }
@GetMapping("/401") public String accessDenied() { return "401"; }
@GetMapping("/user/common") public String common() { return "user/common"; }
@GetMapping("/user/admin") public String admin() { return "user/admin"; }
login.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>登錄</title> </head> <body> <h1>Login page</h1> <p th:if="${loginError}" class="error">用戶名或密碼錯誤</p> <form th:action="@{/login}" method="post"> <label for="username">用戶名</label>: <input type="text" id="username" name="username" autofocus="autofocus" /> <br/> <label for="password">密 碼</label>: <input type="password" id="password" name="password" /> <br/> <input type="submit" value="登錄" /> </form> <p><a href="/index" th:href="@{/index}"></a></p> </body> </html>
index.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <head> <meta charset="UTF-8"> <title>首頁</title> </head> <body> <h2>page list</h2> <a href="/user/common">common page</a> <br/> <a href="/user/admin">admin page</a> <br/> <form th:action="@{/logout}" method="post"> <input type="submit" class="btn btn-primary" value="註銷"/> </form> </body> </html>
admin.html
<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>admin page</title> </head> <body> success admin page!!! </body> </html>
common.html
<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>common page</title> </head> <body> success common page!!! </body> </html>
401.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>401 page</title> </head> <body> <div> <div> <h2>許可權不夠</h2> <p>拒絕訪問!</p> </div> </div> </body> </html>
最後運行項目,可以分別用 user、admin 賬號 去測試認證和授權是否正確。