Spring Boot Security 詳解


Spring Boot Security 詳解


簡介

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

工作流程

從網上找了一張Spring Security 的工作流程圖,如下。


Spring Boot Security 詳解


圖中標記的MyXXX,就是我們項目中需要配置的。

快速上手

建表

表結構


Spring Boot Security 詳解


建表語句

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

pom.xml

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

application.yml

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

User

<code>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;    }    }/<role>/<role>/<role>/<code>

上面的 User 類實現了 UserDetails 接口,該接口是實現Spring Security 認證信息的核心接口。其中 getUsername 方法為 UserDetails 接口 的方法,這個方法返回 username,也可以是其他的用戶信息,例如手機號、郵箱等。getAuthorities() 方法返回的是該用戶設置的權限信息,在本實例中,從數據庫取出用戶的所有角色信息,權限信息也可以是用戶的其他信息,不一定是角色信息。另外需要讀取密碼,最後幾個方法一般情況下都返回 true,也可以根據自己的需求進行業務判斷。

Role

<code>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;    }}/<code>

Role 類實現了 GrantedAuthority 接口,並重寫 getAuthority() 方法。權限點可以為任何字符串,不一定非要用角色名。

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

MyUserDetailsService

<code>@Servicepublic 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;    }    }/<role>/<code>

Service 層需要實現 UserDetailsService 接口,該接口是根據用戶名獲取該用戶的所有信息, 包括用戶信息和權限點。

MyInvocationSecurityMetadataSourceService

<code>@Componentpublic class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {    @Autowired    private PermissionMapper permissionMapper;    /**     * 每一個資源所需要的角色 Collection<configattribute>決策器會用到     */    private static HashMap<string>> 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 );            }        }    }}/<configattribute>/<rolepermisson>/<configattribute>/<string>/<configattribute>/<string>/<configattribute>/<code>

MyInvocationSecurityMetadataSourceService 類實現了 FilterInvocationSecurityMetadataSource,FilterInvocationSecurityMetadataSource 的作用是用來儲存請求與權限的對應關係。

FilterInvocationSecurityMetadataSource接口有3個方法:

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

MyAccessDecisionManager

<code>/** * 決策器 */@Componentpublic 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;    }}/<configattribute>/<configattribute>/<code>

MyAccessDecisionManager 類實現了AccessDecisionManager接口,AccessDecisionManager是由AbstractSecurityInterceptor調用的,它負責鑑定用戶是否有訪問對應資源(方法或URL)的權限。

MyFilterSecurityInterceptor

<code>@Componentpublic 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;    }        }/<code>

每種受支持的安全對象類型(方法調用或Web請求)都有自己的攔截器類,它是AbstractSecurityInterceptor的子類,AbstractSecurityInterceptor 是一個實現了對受保護對象的訪問進行攔截的抽象類。

AbstractSecurityInterceptor的機制可以分為幾個步驟:

  • 1. 查找與當前請求關聯的“配置屬性(簡單的理解就是權限)”
  • 2. 將 安全對象(方法調用或Web請求)、當前身份驗證、配置屬性 提交給決策器(AccessDecisionManager)
  • 3. (可選)更改調用所根據的身份驗證
  • 4. 允許繼續進行安全對象調用(假設授予了訪問權)
  • 5. 在調用返回之後,如果配置了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安全設置。

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

MainController

<code>@Controllerpublic 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";    }}/<code>

頁面

login.html

<code>        <title>登錄/<title>    

Login page

用戶名或密碼錯誤

/<code>

index.html

<code>        <title>首頁/<title>    

page list



/<code>

admin.html

<code>        <title>admin page/<title>    success admin page!!!/<code>

common.html

<code>        <title>common page/<title>    success common page!!!/<code>


分享到:


相關文章: