Spring Security安全管理

Spring Security安全管理

目前主流的安全管理框架主要有Spring Security和Shiro畜侦。Shiro是一個(gè)輕量級(jí)框架,配置較為簡(jiǎn)單澎语。而Spring Security則較為復(fù)雜擅羞,但功能相對(duì)較多减俏。

Spring Boot 中對(duì)Spring Security做了一系列自動(dòng)化配置垄懂,使得在Spring Boot中使用Spring Security相當(dāng)方便草慧。

Spring Security

當(dāng)引入Spring Security依賴后漫谷,所有的接口都將被保護(hù)起來(lái)舔示,訪問(wèn)接口時(shí)需要輸入用戶名和密碼竖共。用戶名默認(rèn)為user俺祠,密碼在控制臺(tái)隨機(jī)生成蜘渣。這是spring boot 為spring security提供的自動(dòng)化配置蔫缸。

當(dāng)然拾碌,登錄的用戶名可以自己配置倦沧,配置的方法主要有兩種

  • 在配置文件中配置

在application.properties中添加如下配置展融,即可設(shè)置登錄的用戶名和密碼

spring.security.user.name=admin
spring.security.user.password=123
spring.security.user.roles=admin
  • 使用Java代碼配置
    創(chuàng)建Security配置類告希,繼承自WebSecurityConfigurerAdapter類燕偶,重寫
    configure(AuthenticationManagerBuilder auth)方法酝惧,如下:
 @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("admin").password("123").roles("admin")
            .and()
            .withUser("zby").password("123").roles("user");
    }

這里設(shè)置了兩個(gè)用戶admin和zby晚唇,用戶角色分別是admin和user哩陕。

HttpSecurity

HttpSecurity是Spring Security Config用于配置http請(qǐng)求安全控制的安全構(gòu)建器(類似于Spring Security XML配置中的http命名空間配置部分)悍及,它的構(gòu)建目標(biāo)是一個(gè)SecurityFilterChain,實(shí)現(xiàn)類使用DefaultSecurityFilterChain心赶。該目標(biāo)SecurityFilterChain最終會(huì)被Spring Security的安全過(guò)濾器FilterChainProxy所持有和應(yīng)用于相應(yīng)的http請(qǐng)求的安全控制园担。

spring security類中為我們提供了configure(HttpSecurity http)弯汰,可以在這個(gè)方法中配置攔截規(guī)則咏闪,實(shí)現(xiàn)http請(qǐng)求的安全管理

使用方法如下:

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() //開啟配置
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("user/**").hasAnyRole("admin","user")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/doLogin")
                .permitAll()
                .and()
                .csrf().disable();
    }

控制器

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }
    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }
}

這里設(shè)置了admin、user角色的訪問(wèn)權(quán)限据某,/admin的接口僅允許角色為admin的用戶訪問(wèn)癣籽,/user接口角色為user和admin用戶都可訪問(wèn)筷狼,其他頁(yè)面登錄后即可訪問(wèn)埂材。
登錄zby用戶后俏险,若要訪問(wèn)/admin/hello接口竖独,瀏覽將會(huì)報(bào)錯(cuò)预鬓,顯示沒(méi)有權(quán)限

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Tue Feb 25 15:11:50 CST 2020
There was an unexpected error (type=Forbidden, status=403).
Forbidden

/hello和/user/hello則可以正常訪問(wèn)格二。

配置多個(gè)HttpSecurity

  @Configuration
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("admin");
        }
    }

    @Configuration
    public static class OtherSecurity extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/login")
                    .permitAll()
                    .and()
                    .csrf().disable();
        }
    }

表單登錄配置

配置表單登錄時(shí)我們可以在 successHandler方法中沧奴,配置登錄成功的回調(diào)滔吠,如果是前后端分離開發(fā)的話疮绷,登錄成功后返回 JSON 即可冬骚,同理,failureHandler 方法中配置登錄失敗的回調(diào),logoutSuccessHandler 中則配置注銷成功的回調(diào)舍悯。

//登錄成功的處理器
.successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        Map<String, Object> map = new HashMap<>();
        map.put("status",200);
        map.put("msg",authentication.getPrincipal());
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
})

登錄成功后返回的json

{
    "msg": {
        "password": null,
        "username": "admin",
        "authorities": [
            {
                "authority": "ROLE_admin"
            }
        ],
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true
    },
    "status": 200
}

登錄失敗的處理器

 //登錄失敗的處理器
.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();
        Map<String, Object> map = new HashMap<>();
        map.put("status",401);
        if(e instanceof LockedException){
            map.put("msg","賬號(hào)被鎖定,登錄失敗");
        }else if(e instanceof BadCredentialsException){
            map.put("msg","用戶名或密碼錯(cuò)誤,登錄失敗");
        }
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
})

注銷登錄

.logoutSuccessHandler(new LogoutSuccessHandler() {
    @Override
    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        Map<String, Object> map = new HashMap<>();
        map.put("status",200);
        map.put("msg","注銷成功");
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
})

注銷成功,返回的json

{
    "msg": "注銷成功",
    "status": 200
}

密碼加密

在Spring5之后奄薇,密碼必須加密后才能應(yīng)用馁蒂。加密密碼則需要配置一個(gè)密碼的編碼器,可以通過(guò)PasswordEncoder實(shí)現(xiàn)

spring security中提供了BCryptPasswordEncoder工具進(jìn)行密碼加密饵隙,如將同一串進(jìn)行十次加密

  @Test
    void contextLoads() {
        for (int i = 0; i < 10; i++) {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            System.out.println(encoder.encode("123"));
        }
    }

得到結(jié)果如下:


image

同一串字符每次加密產(chǎn)生的結(jié)構(gòu)不同金矛,這就實(shí)現(xiàn)了密碼的加密驶俊。

方法安全

Spring Security框架支持通過(guò)在方法上加注解來(lái)確保方法的安全饼酿。

方法安全在Spring Security中默認(rèn)是沒(méi)有開啟的故俐,在Spring Security配置類上加@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true),開啟方法安全的相關(guān)注解

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MultiHttpSecurityConfig{
    ......
}

創(chuàng)建Service類

@Service
public class MethodService {
   @PreAuthorize("hasRole('admin')")
    public String admin(){
        return "hello admin";
    }
    @Secured("ROLE_user")
    public String user(){
        return "hello user";
    }
    @PreAuthorize("hasAnyRole('admin','author')")
    public String hello(){
        return "hello world";
    }
}

為三個(gè)方法分別賦予相應(yīng)的角色。

在Controller中調(diào)用三個(gè)方法

@Autowired
    MethodService methodService;
    @GetMapping("/hello1")
    public String hello1(){
        return methodService.admin();
    }
    @GetMapping("/hello2")
    public String hello2(){
        return methodService.user();
    }
    @GetMapping("/hello3")
    public String hello3(){
        return methodService.hello();
    }

這時(shí)每個(gè)接口都可以被訪問(wèn)刚陡,但只有相應(yīng)的角色才能調(diào)用接口中的方法。

基于數(shù)據(jù)庫(kù)的認(rèn)證

創(chuàng)建項(xiàng)目后并配置數(shù)據(jù)庫(kù)信息

spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

創(chuàng)建User和Role的實(shí)體類

@Data
public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;
    //返回用戶所以角色
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
    //賬戶是否未過(guò)期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    //賬戶是否未鎖定
    @Override
    public boolean isAccountNonLocked() {
        return locked;
    }
    //密碼是否未過(guò)期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    //是否可用
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

這里定義用戶時(shí)需要實(shí)現(xiàn)UserDetails接口。

@Data
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}

Service:

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException("用戶名不存在");
        }
        user.setRoles(userMapper.getUserRolesById(user.getId()));
        return user;
    }
}

配置SpringSecurity

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

角色繼承的配置

@Bean
RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
    roleHierarchy.setHierarchy(hierarchy);
    return roleHierarchy;
}

配置完成后,接下來(lái)指定角色和資源的對(duì)應(yīng)關(guān)系即可股淡,如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/admin/**")
            .hasRole("admin")
            .antMatchers("/db/**")
            .hasRole("dba")
            .antMatchers("/user/**")
            .hasRole("user")
            .and()
            .formLogin()
            .loginProcessingUrl("/doLogin")
            .permitAll()
            .and()
            .csrf().disable();
}

這個(gè)表示 /db/** 格式的路徑需要具備 dba 角色才能訪問(wèn), /admin/** 格式的路徑則需要具備 admin 角色才能訪問(wèn)埠帕, /user/** 格式的路徑敛瓷,則需要具備 user 角色才能訪問(wèn)呐籽,此時(shí)提供相關(guān)接口狡蝶,會(huì)發(fā)現(xiàn)牢酵,dba 除了訪問(wèn) /db/** 馍乙,也能訪問(wèn) /admin/** 和 /user/** 撑瞧,admin 角色除了訪問(wèn) /admin/** 预伺,也能訪問(wèn) /user/** 酬诀,user 角色則只能訪問(wèn) /user/** 瞒御。

動(dòng)態(tài)權(quán)限配置

動(dòng)態(tài)權(quán)限配置就是要將權(quán)限也存入數(shù)據(jù)庫(kù)中肴裙,通過(guò)數(shù)據(jù)庫(kù)中數(shù)據(jù)之間的關(guān)系來(lái)確定權(quán)限蜻懦。
數(shù)據(jù)庫(kù)權(quán)限如下圖所示
[圖片上傳失敗...(image-a9aca8-1582887592116)]
通過(guò)user確定role宛乃,在通過(guò)role定位到相應(yīng)的menu

要實(shí)現(xiàn)動(dòng)態(tài)權(quán)限配置烤惊,首先要配置過(guò)濾器渡贾,創(chuàng)建一個(gè)filter類空骚,實(shí)現(xiàn)FilterInvocationSecurityMetadataSource接口囤屹,并實(shí)現(xiàn)接口中的方法

@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
    //路徑匹配符
    AntPathMatcher pathMatcher = new AntPathMatcher();
    @Autowired
    MenuService menuService;
    //根據(jù)請(qǐng)求地址肋坚,分析請(qǐng)求需要的角色
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        List<Menu> allMenus = menuService.getAllMenus();
        for (Menu menu : allMenus) {
            if(pathMatcher.match(menu.getPattern(),requestUrl)){
                List<Role> roles = menu.getRoles();
                String[] rolesStr = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    rolesStr[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(rolesStr);
            }
        }
        return SecurityConfig.createList("ROLE_login");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

另外還需要配置一個(gè)類用于查詢是否具備請(qǐng)求需要的角色,若不存在則該請(qǐng)求是一個(gè)非法請(qǐng)求铣鹏,該類要實(shí)現(xiàn)AccessDecisionManager接口。該接口提供了三個(gè)方法decide方法和兩個(gè)supports方法合溺,兩個(gè)supports方法默認(rèn)返回值為true辫愉。decide方法中有三個(gè)參數(shù)

void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection)
  • authentication:保存當(dāng)前登錄用戶信息,代表用戶擁有的角色
  • o:是一個(gè)FilterInvocation對(duì)象依疼,用于獲取當(dāng)前請(qǐng)求對(duì)象,代表需要的角色
  • collection:是MyFilter類中Collection<ConfigAttribute> getAttributes(Object o)的返回值
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute attribute : collection) {
            if("ROLE_login".equals(attribute.getAttribute())){
                //判斷是否登錄误辑,若是匿名用戶則表示沒(méi)有登錄巾钉,拋出異常
                if(authentication instanceof AnonymousAuthenticationToken){
                    throw new AccessDeniedException("非法請(qǐng)求潦匈!");
                }else break;
            }
            //獲取當(dāng)前用戶具備的角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if(authority.getAuthority().equals(attribute.getAttribute())){
                    break;
                }
            }
        }
        throw new AccessDeniedException("非法請(qǐng)求茬缩!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

兩個(gè)類寫完之后凰锡,在security配置類中引入,并在HttpSecurity方法中做如下配置:

@Autowired
MyFilter myFilter;
@Autowired
MyAccessDecisionManager myAccessDecisionManager;

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                    o.setAccessDecisionManager(myAccessDecisionManager);
                    o.setSecurityMetadataSource(myFilter);
                    return o;
                }
            })
            .and()
            .formLogin()
            .permitAll()
            .and()
            .csrf().disable();
    }

編寫接口測(cè)試

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
    @GetMapping("/db/hello")
    public String db(){
        return "hello db";
    }

    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }

    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }
}

OAuth2協(xié)議

OAuth(Open Authorization,開放授權(quán))是為用戶資源的授權(quán)定義了一個(gè)安全智绸、開放及簡(jiǎn)單的標(biāo)準(zhǔn)瞧栗,第三方無(wú)需知道用戶的賬號(hào)及密碼,就可獲取到用戶的授權(quán)信息

  • 應(yīng)用場(chǎng)景

第三方應(yīng)用授權(quán)登錄:在APP或者網(wǎng)頁(yè)接入一些第三方應(yīng)用時(shí)殴边,時(shí)常會(huì)需要用戶登錄另一個(gè)合作平臺(tái)锤岸,比如QQ是偷,微博蛋铆,微信的授權(quán)登錄,第三方應(yīng)用通過(guò)oauth2方式獲取用戶信息

具體的實(shí)現(xiàn)流程圖如下:


  • Spring Security結(jié)合OAuth2
    Spring Boot下的OAuth2是在spring security的基礎(chǔ)上完成的留特。
    添加OAuth2的依賴:
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.6.RELEASE</version>
</dependency>

將OAuth中的Token令牌放在Redis中,因此需要再添加Redis依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置Redis

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0

在OAuth2中需要配置兩個(gè)服務(wù)器捧韵,一個(gè)授權(quán)服務(wù)器和一個(gè)資源服務(wù)器
1.配置授權(quán)服務(wù)器

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("password")
                .authorizedGrantTypes("password","refresh_token")//配置授權(quán)模式
                .accessTokenValiditySeconds(1800)//Token過(guò)期時(shí)間
                .resourceIds("rid")
                .scopes("all")
                .secret("$2a$10$9zMfB82E5BnYvnKriQUdaudC39H5JEu.HN80ywI2EQY/2.MuOj.i.");
    }

    //配置Token存取
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    //支持clientId和client security做登錄認(rèn)證
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
    }

2.配置資源服務(wù)器

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("rid").stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated();
    }
}

根據(jù)OAuth2協(xié)議市咆,先從授權(quán)服務(wù)器中獲取Token,再到資源服務(wù)器上獲取資源再来,判斷給出的Token令牌是否有權(quán)限訪問(wèn)資源蒙兰。

最后配置Security

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        return super.userDetailsService();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("zby")
                .password("$2a$10$9zMfB82E5BnYvnKriQUdaudC39H5JEu.HN80ywI2EQY/2.MuOj.i.")
                .roles("admin")
                .and()
                .withUser("user")
                .password("$2a$10$9zMfB82E5BnYvnKriQUdaudC39H5JEu.HN80ywI2EQY/2.MuOj.i.")
                .roles("user");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/oauth/**")
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .and().csrf().disable();
    }
}
  • 測(cè)試
    在PostMan中,向測(cè)試接口發(fā)送請(qǐng)求芒篷,可得到token


    image

Redis中存儲(chǔ)的Token信息


image

用Token去訪問(wèn)相應(yīng)資源


image

token過(guò)期時(shí)可利用refresh_token參數(shù),通過(guò)post請(qǐng)求獲取新的token

發(fā)送請(qǐng)求:http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=1235097a-d9fd-4342-9c05-a0c2b535b166&client_id=password&client_secret=123

得到新的token

{
    "access_token": "37a62e16-0774-4fc4-b043-824343b3709b",
    "token_type": "bearer",
    "refresh_token": "1235097a-d9fd-4342-9c05-a0c2b535b166",
    "expires_in": 1799,
    "scope": "all"
}

Spring Security使用Json登錄

keyValue形式的登錄主要通過(guò)過(guò)濾器UsernamePasswordAuthenticationFilter來(lái)實(shí)現(xiàn)。所以针炉,要實(shí)現(xiàn)Json登錄需要重新一個(gè)過(guò)濾器挠他。

創(chuàng)建過(guò)濾器MyFilter類,繼承UsernamePasswordAuthenticationFilter并重寫attemptAuthentication方法篡帕。

public class MyFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //先判斷發(fā)來(lái)的是否是Post請(qǐng)求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        //解析Json
        if(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
            //若if條件成立殖侵,說(shuō)明用戶以JSON形式傳遞參數(shù)
            String username = null;
            String password = null;

            try {
                Map<String,String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                username = map.get("username");
                password = map.get("password");
            } catch (IOException e) {
                e.printStackTrace();
            }


            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        //否則調(diào)用父類的方法登錄
        return super.attemptAuthentication(request, response);
    }
}

之后,在Security配置類中進(jìn)行配置镰烧,使MyFilter中的方法生效

整合JWT

JWT拢军,全稱是Json Web Token,是一種JSON風(fēng)格的輕量級(jí)的授權(quán)和身份認(rèn)證規(guī)范怔鳖,可實(shí)現(xiàn)無(wú)狀態(tài)茉唉、分布式的Web應(yīng)用授權(quán)。特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場(chǎng)景结执。

jwt數(shù)據(jù)格式

jwt數(shù)據(jù)格式一般包括三部分:

1.頭部(Header)

頭部用于描述關(guān)于該JWT的最基本的信息度陆,例如其類型以及簽名所用的算法等。這也可以被表示成一個(gè)JSON對(duì)象献幔。對(duì)頭部進(jìn)行Base64Url編碼(可解碼)坚芜,得到第一部分?jǐn)?shù)據(jù)。

2.載荷(Payload)

就是有效數(shù)據(jù)斜姥,在官方文檔中(RFC7519),這里給了7個(gè)示例信息:

  • iss (issuer):表示簽發(fā)人
  • exp (expiration time):表示token過(guò)期時(shí)間
  • sub (subject):主題
  • aud (audience):受眾
  • nbf (Not Before):生效時(shí)間
  • iat (Issued At):簽發(fā)時(shí)間
  • jti (JWT ID):編號(hào)
    這部分也會(huì)采用Base64Url編碼,得到第二部分?jǐn)?shù)據(jù)铸敏。

3.簽名(Signature)

是整個(gè)數(shù)據(jù)的認(rèn)證信息缚忧。一般根據(jù)前兩步的數(shù)據(jù),再加上服務(wù)的的密鑰secret(密鑰保存在服務(wù)端杈笔,不能泄露給客戶端)闪水,通過(guò)Header中配置的加密算法生成。用于驗(yàn)證整個(gè)數(shù)據(jù)完整和可靠性蒙具。

將這三部分用.連接成一個(gè)完整的字符串,構(gòu)成了最終的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT交互流程

1.應(yīng)用程序或客戶端向授權(quán)服務(wù)器請(qǐng)求授權(quán)
2.獲取到授權(quán)后球榆,授權(quán)服務(wù)器會(huì)向應(yīng)用程序返回訪問(wèn)令牌
3、應(yīng)用程序使用訪問(wèn)令牌來(lái)訪問(wèn)受保護(hù)資源(如API)

因?yàn)镴WT簽發(fā)的token中已經(jīng)包含了用戶的身份信息禁筏,并且每次請(qǐng)求都會(huì)攜帶持钉,這樣服務(wù)的就無(wú)需保存用戶信息,甚至無(wú)需去數(shù)據(jù)庫(kù)查詢篱昔,這樣就完全符合了RESTful的無(wú)狀態(tài)規(guī)范每强。

在Spring Security中整合JWT

首先創(chuàng)建一個(gè)Spring Boot項(xiàng)目,創(chuàng)建時(shí)添加Spring Security依賴州刽,創(chuàng)建完成后空执,添加 jjwt 依賴,pom.xml文件如下:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

然后在項(xiàng)目中創(chuàng)建一個(gè)簡(jiǎn)單的 User 對(duì)象實(shí)現(xiàn) UserDetails 接口穗椅。

再創(chuàng)建一個(gè)HelloController辨绊,內(nèi)容如下:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello jwt !";
    }
    @GetMapping("/admin")
    public String admin() {
        return "hello admin !";
    }
}

HelloController有兩個(gè)接口,設(shè)計(jì)是 /hello 接口可以被具有 user 角色的用戶訪問(wèn)匹表,而 /admin 接口則可以被具有 admin 角色的用戶訪問(wèn)门坷。

接下來(lái)提供兩個(gè)和 JWT 相關(guān)的過(guò)濾器配置:

一個(gè)是用戶登錄的過(guò)濾器,在用戶的登錄的過(guò)濾器中校驗(yàn)用戶是否登錄成功桑孩,如果登錄成功拜鹤,則生成一個(gè)token返回給客戶端,登錄失敗則給前端一個(gè)登錄失敗的提示流椒。用戶登錄的過(guò)濾器 JwtLoginFilter 繼承自 AbstractAuthenticationProcessingFilter敏簿,并實(shí)現(xiàn)其中的三個(gè)默認(rèn)方法。
在attemptAuthentication方法中宣虾,從登錄參數(shù)中提取出用戶名密碼惯裕,然后調(diào)用AuthenticationManager.authenticate()方法去進(jìn)行自動(dòng)校驗(yàn)。

如果校驗(yàn)成功绣硝,就會(huì)來(lái)到successfulAuthentication回調(diào)中蜻势,在successfulAuthentication方法中,將用戶角色遍歷然后用一個(gè)“鹉胖,”連接起來(lái)握玛,然后再利用Jwts去生成token够傍,按照代碼的順序,生成過(guò)程一共配置了四個(gè)參數(shù)挠铲,分別是用戶角色冕屯、主題、過(guò)期時(shí)間以及加密算法和密鑰拂苹,然后將生成的token寫出到客戶端安聘。

如果校驗(yàn)失敗就會(huì)來(lái)到unsuccessfulAuthentication方法中,在這個(gè)方法中返回一個(gè)錯(cuò)誤提示給客戶端即可瓢棒。

public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
    public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException {
        User user = new ObjectMapper().readValue(req.getInputStream(),User.class);
        return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword()));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();//獲取登錄用戶角色
        StringBuffer sb = new StringBuffer();
        for (GrantedAuthority authority : authorities) {
            sb.append(authority.getAuthority()).append(",");
        }
        String jwt = Jwts.builder()
                .claim("authorities", sb)
                .setSubject(authResult.getName())
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
                .signWith(SignatureAlgorithm.HS512, "zby@123")
                .compact();//生成JWT的Token
        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);
        map.put("msg","登錄成功");
        resp.setContentType("application/json:charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
        Map<String,String> map = new HashMap<>();
        map.put("msg","登錄失敗");
        resp.setContentType("application/json:charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
}

第二個(gè)過(guò)濾器則是當(dāng)其他請(qǐng)求發(fā)送來(lái)浴韭,校驗(yàn)token的過(guò)濾器,如果校驗(yàn)成功脯宿,就讓請(qǐng)求繼續(xù)執(zhí)行念颈。首先從請(qǐng)求頭中提取出authorization字段,這個(gè)字段對(duì)應(yīng)的value就是用戶的token嗅绰。將提取出來(lái)的token字符串轉(zhuǎn)換為一個(gè)Claims對(duì)象舍肠,再?gòu)腃laims對(duì)象中提取出當(dāng)前用戶名和用戶角色,創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken放到當(dāng)前的Context中窘面,然后執(zhí)行過(guò)濾鏈?zhǔn)拐?qǐng)求繼續(xù)執(zhí)行下去翠语。

public class JwtFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        String jwtToken = req.getHeader("authorization");
        Jws<Claims> jws = Jwts.parser().setSigningKey("zby@123")
                .parseClaimsJws(jwtToken.replace("Bearer", ""));
        Claims claims = jws.getBody();
        String username = claims.getSubject();
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(((String) claims.get("authorities")));
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

兩個(gè)過(guò)濾器配置好后,在Security配置類中添加兩個(gè)過(guò)濾器

 @Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/hello")
        .hasRole("user")
        .antMatchers("/admin")
        .hasRole("admin")
        .antMatchers(HttpMethod.POST,"/login")
        .permitAll()
        .anyRequest().authenticated()
        .and()
        .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()), UsernamePasswordAuthenticationFilter.class)
         .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class)
         .csrf().disable();
    }

配置路徑規(guī)則時(shí)财边, /hello 接口必須要具備 user 角色才能訪問(wèn)肌括, /admin 接口必須要具備 admin 角色才能訪問(wèn),POST 請(qǐng)求并且是 /login 接口則可以直接通過(guò)酣难,其他接口必須認(rèn)證后才能訪問(wèn)谍夭。

登陸成功,返回一個(gè)Json

{
    "msg": "登錄成功",
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfYWRtaW4sIiwic3ViIjoiYWRtaW4iLCJleHAiOjE1ODI3OTM0NjR9.4cTTZpjl1j2YxldmTHWbK6oN0htJn-kW9V2p6Nj7jc26znegUmtrXohy0dgH4uDH053UL4-IICSo_ETzJJtmeQ"
}

登錄成功后返回一個(gè)token憨募,請(qǐng)求資源時(shí)需要提供token才能正常訪問(wèn)


image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末紧索,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子菜谣,更是在濱河造成了極大的恐慌珠漂,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尾膊,死亡現(xiàn)場(chǎng)離奇詭異媳危,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)冈敛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門待笑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人抓谴,你說(shuō)我怎么就攤上這事暮蹂∧欤” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵仰泻,是天一觀的道長(zhǎng)第租。 經(jīng)常有香客問(wèn)我,道長(zhǎng)我纪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任丐吓,我火速辦了婚禮浅悉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘券犁。我一直安慰自己术健,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布粘衬。 她就那樣靜靜地躺著荞估,像睡著了一般。 火紅的嫁衣襯著肌膚如雪稚新。 梳的紋絲不亂的頭發(fā)上勘伺,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音褂删,去河邊找鬼飞醉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛屯阀,可吹牛的內(nèi)容都是我干的缅帘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼难衰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钦无!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起盖袭,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤失暂,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后苍凛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體趣席,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年醇蝴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宣肚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡悠栓,死狀恐怖霉涨,靈堂內(nèi)的尸體忽然破棺而出按价,到底是詐尸還是另有隱情,我是刑警寧澤笙瑟,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布楼镐,位于F島的核電站,受9級(jí)特大地震影響往枷,放射性物質(zhì)發(fā)生泄漏框产。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一错洁、第九天 我趴在偏房一處隱蔽的房頂上張望秉宿。 院中可真熱鬧,春花似錦屯碴、人聲如沸描睦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)忱叭。三九已至,卻和暖如春今艺,著一層夾襖步出監(jiān)牢的瞬間韵丑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工洼滚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留埂息,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓遥巴,卻偏偏與公主長(zhǎng)得像千康,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子铲掐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容