springCloud-依賴Spring Security使用 JWT實現無狀態的分佈式會話

案例

在前後端分離的,後端微服務的情況下,會話已經不再適合保存在服務端,使用redis可以保存會話,但是需要在redis集羣之間進行復制,如果用戶較多,保存的數據量也比較大。

JWT實現無狀態的會話機制,是一個解決方案。

1、什麼是無狀態?

微服務集羣中的每個服務,對外提供的都使用RESTful風格的接口。而RESTful風格的一個最重要的規範就是:服務的無狀態性,即:

1、服務端不保存任何客戶端請求者信息
2、客戶端的每次請求必須具備自描述信息,通過這些信息識別客戶端身份

2、如何實現無狀態?

1、首先客戶端發送賬戶名/密碼到服務端進行認證
2、認證通過後,服務端將用戶信息加密並且編碼成一個token,返回給客戶端
3、以後客戶端每次發送請求,都需要攜帶認證的token
4、服務端對客戶端發送來的token進行解密,判斷是否有效,並且獲取用戶登錄信息

3、JWT

JWT 作爲一種規範,並沒有和某一種語言綁定在一起,常用的Java 實現是GitHub 上的開源項目 jjwt,地址如下:https://github.com/jwtk/jjwt

代碼

說明:

1、一個登陸接口/login,用於獲取token
2、一個用戶接口/hello,用戶角色可以訪問
3、一個管理接口/admin,管理角色可以訪問

1、啓動文件

package com.baiziwan.authorize;

import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan({"com.baiziwan.authorize.*.dao", "com.baiziwan.authorize.*.*.dao"})
@EnableAsync
public class AuthorizeApplication {
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizeApplication.class);

    public static void main(String[] args) {
        try {
            SpringApplication.run(AuthorizeApplication.class, args);
        } catch (Exception e) {
            LOGGER.error("啓動失敗!", e);
        }
    }
}

2、pom文件主要依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
            <version>5.1.3.RELEASE</version>
        </dependency>

3、User文件

package com.baiziwan.authorize.model;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class User implements UserDetails {
    private String username;
    private String password;
    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    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 true;
    }

}

4、控制器url

package com.baiziwan.authorize.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    //需要jwt訪問,user角色可以訪問
    @GetMapping("/hello")
    public String hello() {
        return "hello jwt !";
    }

    //需要jwt訪問,admin角色可以訪問
    @GetMapping("/admin")
    public String admin() {
        return "hello admin !";
    }

}

5、JWT 過濾器配置

1、一個是用戶登錄的過濾器,在用戶的登錄的過濾器中校驗用戶是否登錄成功,如果登錄成功,則生成一個token返回給客戶端,登錄失敗則給前端一個登錄失敗的提示。
2、第二個過濾器則是當其他請求發送來,校驗token的過濾器,如果校驗成功,就讓請求繼續執行。

package com.baiziwan.authorize.filter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;

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");
        System.out.println(jwtToken);
        Claims claims = Jwts.parser().setSigningKey("sang@123").parseClaimsJws(jwtToken.replace("Bearer",""))
                .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(req,servletResponse);
    }
}
package com.baiziwan.authorize.filter;

import com.baiziwan.authorize.model.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Date;

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 as = new StringBuffer();
        for (GrantedAuthority authority : authorities) {
            as.append(authority.getAuthority())
                    .append(",");
        }
        String jwt = Jwts.builder()
                .claim("authorities", as)//配置用戶角色
                .setSubject(authResult.getName())
                .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
                .signWith(SignatureAlgorithm.HS512,"sang@123")
                .compact();
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(jwt));
        out.flush();
        out.close();
    }
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write("登錄失敗!");
        out.flush();
        out.close();
    }
}

6、Spring Security 配置

1、簡單起見,這裏並未連接數據庫,我直接在內存中配置了兩個用戶,兩個用戶具備不同的角色。
2、配置路徑規則時, /hello 接口必須要具備 user 角色才能訪問, /admin 接口必須要具備 admin 角色才能訪問,POST 請求並且是 /login 接口則可以直接通過,其他接口必須認證後才能訪問。

package com.baiziwan.authorize.config;

import com.baiziwan.authorize.filter.JwtFilter;
import com.baiziwan.authorize.filter.JwtLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin")
                .password("123").roles("admin")
                .and()
                .withUser("sang")
                .password("456")
                .roles("user");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.GET,"/.well-known/jwks.json").permitAll()
                .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();
    }
}

7、測試效果

1、獲取token
springCloud-依賴Spring Security使用 JWT實現無狀態的分佈式會話

2、帶令牌訪問
springCloud-依賴Spring Security使用 JWT實現無狀態的分佈式會話

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章