Spring Boot Security 詳解

簡介

Spring Security,這是一種基於 Spring AOP 和 Servlet 過濾器的安全框架。它提供全面的安全性解決方案,同時在 Web 請求級和方法調用級處理身份確認和受權。css

工做流程

從網上找了一張Spring Security 的工做流程圖,以下。html

圖中標記的MyXXX,就是咱們項目中須要配置的。java

快速上手

建表

表結構mysql

建表語句git

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);

pom.xml

<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>

application.yml

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

User

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,也能夠根據本身的需求進行業務判斷。程序員

Role

public class Role implements GrantedAuthority {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getAuthority() {
        return name;
    }

}

Role 類實現了 GrantedAuthority 接口,並重寫 getAuthority() 方法。權限點能夠爲任何字符串,不必定非要用角色名。github

全部的Authentication實現類都保存了一個GrantedAuthority列表,其表示用戶所具備的權限。GrantedAuthority是經過AuthenticationManager設置到Authentication對象中的,而後AccessDecisionManager將從Authentication中獲取用戶所具備的GrantedAuthority來鑑定用戶是否具備訪問對應資源的權限。web

MyUserDetailsService

@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 接口,該接口是根據用戶名獲取該用戶的全部信息, 包括用戶信息和權限點。spring

MyInvocationSecurityMetadataSourceService

@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 的做用是用來儲存請求與權限的對應關係。sql

FilterInvocationSecurityMetadataSource接口有3個方法:

  • boolean supports(Class<?> clazz):指示該類是否可以爲指定的方法調用或Web請求提供ConfigAttributes。
  • Collection<ConfigAttribute> getAllConfigAttributes():Spring容器啓動時自動調用, 通常把全部請求與權限的對應關係也要在這個方法裏初始化, 保存在一個屬性變量裏。
  • Collection<ConfigAttribute> getAttributes(Object object):當接收到一個http請求時, filterSecurityInterceptor會調用的方法. 參數object是一個包含url信息的HttpServletRequest實例. 這個方法要返回請求該url所須要的全部權限集合。

MyAccessDecisionManager

/**
 * 決策器
 */
@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)的權限。

MyFilterSecurityInterceptor

@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的機制能夠分爲幾個步驟:

    1. 查找與當前請求關聯的「配置屬性(簡單的理解就是權限)」
    1. 將 安全對象(方法調用或Web請求)、當前身份驗證、配置屬性 提交給決策器(AccessDecisionManager)
    1. (可選)更改調用所根據的身份驗證
    1. 容許繼續進行安全對象調用(假設授予了訪問權)
    1. 在調用返回以後,若是配置了AfterInvocationManager。若是調用引起異常,則不會調用AfterInvocationManager。

AbstractSecurityInterceptor中的方法說明:

  • beforeInvocation()方法實現了對訪問受保護對象的權限校驗,內部用到了AccessDecisionManager和AuthenticationManager;
  • finallyInvocation()方法用於實現受保護對象請求完畢後的一些清理工做,主要是若是在beforeInvocation()中改變了SecurityContext,則在finallyInvocation()中須要將其恢復爲原來的SecurityContext,該方法的調用應當包含在子類請求受保護資源時的finally語句塊中。
  • afterInvocation()方法實現了對返回結果的處理,在注入了AfterInvocationManager的狀況下默認會調用其decide()方法。

瞭解了AbstractSecurityInterceptor,就應該明白了,咱們自定義MyFilterSecurityInterceptor就是想使用咱們以前自定義的 AccessDecisionManager 和 securityMetadataSource。

SecurityConfig

@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( "/" );
    }


}

MainController

@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 帳號 去測試認證和受權是否正確。

參考

《深刻理解Spring Cloud與微服務構建》

https://www.ktanx.com/blog/p/4929

源碼

https://github.com/gf-huanchupk/SpringBootLearning/tree/master/springboot-security





歡迎掃碼或微信搜索公衆號《程序員果果》關注我,關注有驚喜~

相關文章
相關標籤/搜索