天天看點

SpringSecurity使用@PreAuthorize、@PostAuthorize實作Web系統權限認證

SpringSecurity可以使用@PreAuthorize和@PostAuthorize進行通路控制,下面進行說明。

項目使用Springboot2.3.3+SpringSecurtiy+Mbatis實作。

首先先引入pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- 需要單獨添加thymeleaf的布局子產品 -->
        <dependency>
            <groupId>nz.net.ultraq.thymeleaf</groupId>
            <artifactId>thymeleaf-layout-dialect</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.10</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.4.0</version>
        </dependency>
    </dependencies>
           

項目中使用log4j2替代了logback日志系統,需要将log4j2的配置檔案進行配置。

第一步:實作SpringSecurity的配置

擴充WebSecurityConfigurerAdapter的配置類

/**
 * @author MaLei
 * @description: 建立一個WebSecurityConfig類,使其繼 承WebSecurityConfigurerAdapter
 * 在給WebSecutiryConfig類中加上@EnableWebSecurity 注解後,便會自動被 Spring發現并注冊(檢視
 * @EnableWebSecurity 即可看到@Configuration 注解已經存在
 * @create 2020/7/14
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟全局方法配置這個注解必須開啟否則@PreAuthorize等注解不生效
public class WebSecutiryConfig extends WebSecurityConfigurerAdapter {
    //認證管理器配置方法可以配置定定義的UserDetailService和passwordEncoder。無需配置springboot2.3會自動注入bean
   /* @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(UserDetailService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }*/

    //核心過濾器配置方法
    //void configure(WebSecurity web)用來配置 WebSecurity。而 WebSecurity是基于 Servlet Filter用來配置 springSecurityFilterChain。而 springSecurityFilterChain又被委托給了 Spring Security 核心過濾器 Bean DelegatingFilterProxy。  相關邏輯你可以在 WebSecurityConfiguration中找到。一般不會過多來自定義 WebSecurity, 使用較多的使其ignoring()方法用來忽略Spring Security對靜态資源的控制.對于靜态資源的忽略盡量在此處設定,否則容易無限循環重新定向到登入頁面
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/static/**", "/mylogin.html","/admin", "/favicon.ico");
}

    //安全過濾器鍊配置方法
    //void configure(HttpSecurity http)這個是我們使用最多的,用來配置 HttpSecurity。 HttpSecurity用于建構一個安全過濾器鍊 SecurityFilterChain。SecurityFilterChain最終被注入核心過濾器 。 HttpSecurity有許多我們需要的配置。我們可以通過它來進行自定義安全通路政策
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // super.configure(http); 不能使用預設的驗證方式
        //authorizeRequests()方法實際上傳回了一個 URL 攔截注冊器,我們可以調用它提供的
        //anyanyRequest()、antMatchers()和regexMatchers()等方法來比對系統的URL,并為其指定安全
        //政策
       http.authorizeRequests()
                .anyRequest().authenticated() 
                .and()
                //formLogin()方法和httpBasic()方法都聲明了需要Spring Security提供的表單認證方式,分别返
                //回對應的配置器
                .formLogin()
                //,formLogin().loginPage("/myLogin.html")指定自定義的登入
                //頁/myLogin.html,同時,Spring Security會用/myLogin.html注冊一個POST路由,用于接收登入請求
               //loginProcessingUrl("/login")指定的/login必須與表單送出中指向的action一緻
                   .loginPage("/mylogin.html").loginProcessingUrl("/logins").permitAll()
               //表單中使用者名和密碼對應參數設定(預設為username和password),如果是預設值則不用設定下面的參數對應.
               .usernameParameter("usernames").passwordParameter("passwords")
               .successForwardUrl("/hello")
               .failureHandler(new AuthenticationFailureHandler() {
                   @Override
                   public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                       httpServletResponse.setContentType("application/json;charset=UTF-8");
                       httpServletResponse.setStatus(403);
                       String error=new String();
                       if (e instanceof BadCredentialsException ||
                               e instanceof UsernameNotFoundException) {
                           error="賬戶名或者密碼輸入錯誤!";
                       } else if (e instanceof LockedException) {
                           error="賬戶被鎖定,請聯系管理者!";
                       } else if (e instanceof CredentialsExpiredException) {
                           error="密碼過期,請聯系管理者!";
                       } else if (e instanceof AccountExpiredException) {
                           error="賬戶過期,請聯系管理者!";
                       } else if (e instanceof DisabledException) {
                           error="賬戶被禁用,請聯系管理者!";
                       } else {
                           error="登入失敗!";
                       }
                       httpServletResponse.getWriter().write("{\"message\":\""+error+"\"}");
                   }
               })
               .and()
               .logout()
               .logoutUrl("/logout")
               .logoutSuccessHandler(new LogoutSuccessHandler() {
                   @Override
                   public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException {
                       resp.setContentType("application/json;charset=utf-8");

                      @Cleanup PrintWriter out = resp.getWriter();
                       out.write("{\"msg\":\"登出成功!\"}");
                       out.flush();
                      // out.close();
                   }
               })
               .permitAll()
                .and()
                //csrf()方法是Spring Security提供的跨站請求僞造防護功能,當我們繼承WebSecurityConfigurer
                //Adapter時會預設開啟csrf()方法
                .csrf().disable()
               //隻有确實的通路失敗才會進入AccessDeniedHandler,如果是未登陸或者會話逾時等,不會觸發AccessDeniedHandler,而是會直接跳轉到登陸頁面
       .exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
           @Override
           public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
               httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
               httpServletResponse.setContentType("application/json;charset=UTF-8");
               PrintWriter out = httpServletResponse.getWriter();
               out.write(new ObjectMapper().writeValueAsString("{\"message\":\"權限不足,請聯系管理者!\"}"));
               out.flush();
               out.close();
           }
       });

    }
    /**
     * 增加密碼加密器,一旦增加,在驗證過程中security将使用密碼加密器進行加密對比,資料庫中如果存儲明文密碼,在
     * UserDetailsService接口實作方法中,先加密密碼然後才能傳回UserDetails
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder(){
        //使用系統自帶密碼加密器也可以參考上一篇自己繼承PasswordEncoder接口寫編碼器
        return new BCryptPasswordEncoder();
    }
}
           

在配置檔案中,靜态資源、登入頁面等求情不需要登入賬戶即可通路,我放在了

public void configure(WebSecurity web) throws Exception

方法中。

第二步:實作UserDetails及UserDetailsService接口。

User類實作UserDetails接口

public class User implements Serializable, UserDetails {
    private Long id;

    private String username;

    private String password;

    private String name;

    private Boolean enabled;

    private List<Role> roles;

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

    private static final long serialVersionUID = 1L;

    public Long getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public void setUsername(String username) {
        this.username = username == null ? null : username.trim();
    }
    //将目前賬戶的所屬角色進行配置
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> list=new ArrayList<>();
        //将目前使用者的配屬角色填入集合
        Assert.notNull(roles,"角色集合為null");
        for (Role r:roles){
            list.add(new SimpleGrantedAuthority(r.getName()));
        }
        return list.size()>0?list:null;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password == null ? null : password.trim();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name == null ? null : name.trim();
    }

    public Boolean getEnabled() {
        return enabled;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append(" [");
        sb.append("Hash = ").append(hashCode());
        sb.append(", id=").append(id);
        sb.append(", username=").append(username);
        sb.append(", password=").append(password);
        sb.append(", name=").append(name);
        sb.append(", enabled=").append(enabled);
        sb.append("]");
        return sb.toString();
    }
}
           

UserDetailService類實作UserDetailsService接口

/**
 * @author MaLei
 * @description: UserDetailService
 * @create 2020/7/14
 */
@Component
@Slf4j
public class UserDetailService implements UserDetailsService {
    @Autowired
    UserMapper customerMapper;
    @Override
    @Transactional("firstTransactionManager")

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("登入賬号:{}",username);
        //資料庫讀取賬戶,如果讀出來的密碼是明碼必須使用PasswordEncoder加密
        User cust=customerMapper.selectByUserNameContainsRoles(username);
        if(cust==null)
            throw new UsernameNotFoundException("賬戶不存在");
        return cust;
    }
}
           

由于使用Mybatis實作的資料庫操作,具體的mapper實作就不貼出來了。

第三步在前段Controller的方法上使用@PreAuthorize和@PostAuthorize

@PreAuthorize是在方法執行前進行權限認證

@PostAuthorize是方法執行後在傳回前進行權限認證

@RestController
public class TestController {
    @RequestMapping("/hello")
    @PreAuthorize("hasPermission('/hello', 'read') or hasRole('ROLE_admin')")
    public String hello() {
        return  "hello";
}
           

代碼@PreAuthorize(“hasPermission(’/hello’, ‘read’) or hasRole(‘ROLE_admin’)”)中也使用了hasRole(‘ROLE_admin’),這代表目前登入使用者隻要具有ROLE_admin這個角色,即可通路。hasRole(‘ROLE_admin’)中角色名稱可以寫成ROLE_admin也可簡寫admin,系統會自動判斷是否有ROLE_字首,沒有會自動加上。

hasPermission(’/hello’, ‘read’)系統不會自動處理,需要實作PermissionEvaluator接口進行處理。

第四步驟:實作PermissionEvaluator接口

/**
 * @author MaLei
 * @description: PermissionEvaluator接口實作類
 * @create 2020/7/17
 */
@Configuration
public class MyPermissionEvaluator implements PermissionEvaluator {
    @Autowired
    MenuMapper menuMapper;
    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        boolean accessable = false;
        if(authentication.getPrincipal().toString().compareToIgnoreCase("anonymousUser") != 0){
            Menu menu= menuMapper.selectByRequestUrl(targetDomainObject.toString());
            if(menu==null) return accessable;
            List<Role> roles = menu.getRoles();
            Iterator<Role> it=roles.iterator();
            while(it.hasNext()) {
                Role role=it.next();
                for (GrantedAuthority authority : authentication.getAuthorities()) {
                   if(role.getName().equals(authority.getAuthority())){
                       accessable=true;
                   }
                }
            }
        }
        return accessable;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}
           

public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission)

方法是處理

@PreAuthorize(“hasPermission(’/hello’, ‘read’) or hasRole(‘ROLE_admin’)”)

注解中hasPermission(’/hello’, ‘read’) 傳入的資訊的,

hasPermission方法中Authentication authentication參數代表登入使用者及權限資訊

Object targetDomainObject參數代表hasPermission(’/hello’, ‘read’) 中第一個參數"/hello"

Object permission參數代表hasPermission(’/hello’, ‘read’)中第二個參數"read"

在高版本的springboot中,實作了PermissionEvaluator接口後,隻要将實作類加上@Configuration系統會自動進行注冊到容器。

如果低版本springboot中,自定義的PermissionEvaluator實作類後不生效,需要在自己實作的WebSecutiryConfig配置類中聲明一個@Bean,形式如下:

/**
     * 注入自定義PermissionEvaluator
     */
    @Bean
    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler(){
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        //将自己實作的PermissionEvaluator接口實作類加入處理器
        handler.setPermissionEvaluator(new MyPermissionEvaluator());
        return handler;
    }
           

通過以上配置就實作了使用@PreAuthorize、@PostAuthorize等注解來進行權限限制,本例中設定了所有請求必須登入後才能通路,如果一個賬戶登入了,但是請求某一資源後,該資源方法上未加上@PreAuthorize或@PostAuthorize注解,則代表該資源無需通路權限,可以直接通路,這點要注意。

繼續閱讀