Spring Security使用詳解(基本用法 )


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,配置用戶名和密碼

    如果對默認的用戶名和密碼不滿意,可以在 application.properties 中配置默認的用戶名、密碼和角色。這樣項目啟動后就不會隨機生成密碼了,而是使用我們配置的用戶、密碼,並且登錄后還具有一個 admin 角色(關於角色的用法再后面的文章會相信介紹)。
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>
View Code

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 權限,就需要開發者自定義權限配置,具體步驟如下。

四、基於數據庫的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 提示。下面通過樣例演示如何進行登錄表單的個性化配置。

自定義登錄頁面、登錄接口、登錄成功或失敗的處理邏輯

首先修改 Spring Security 配置,增加相關的自定義代碼:
  • 將登錄頁改成使用自定義頁面,並配置登錄請求處理接口,以及用戶密碼提交時使用的參數名。
  • 自定義了登錄成功、登錄失敗的處理邏輯,根據情況返回響應的 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;
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM