1. 程式人生 > 實用技巧 >前後端分離Spring security 從healer的token獲取Session

前後端分離Spring security 從healer的token獲取Session

Cookie->Token

由於HTTP協議是無狀態協議,為了能夠跟蹤使用者的整個會話,常用的是Cookie和Session模式

Cookie通過在客戶端記錄資訊確定使用者身份,Session通過在伺服器記錄確定使用者身份

Cookie在客戶端第一次訪問服務端時,服務端生成Cookie並往客戶端寫入,而且一般都是HttpOnly,無法在客戶端通過JS去讀取這個Cookie。客戶端每次請求都會帶上這個Cookie,服務端根據Cookie來確定對應的是哪一個Session。

Cookie-Session模式,對於單機版完全能夠勝任。但是在分散式環境通常會採用Token-Session模式,Token其實就是一個SessionId,只不過不再是通過Cookie來獲取,而是在請求的Header裡傳入Token值。而服務端的Session為了在分散式環境能夠共享,一般都是放在Redis。

前後端分離

服務端鑑權常用的有Apache Shiro和Spring Security。

現在主要從Spring Security說起,在沒有前後端分離之前,Spring Security除了負責請求攔截,鑑權。還有專門提供跳轉到登入頁面、登入成功和登入失敗頁面的跳轉。

前後端分離之後,服務端只提供REST介面,不再對頁面的渲染和跳轉做控制。

所以對於鑑權失敗,登入成功和失敗,應該直接返回一個JSON資料,而不是頁面跳轉。

Spring security 從header的token獲取Session

解決兩個問題

  • 客戶端通過header傳入token,代替從Cookie獲SessionId
  • 適配前後端分離的鑑權登入(返回JSON資料,而不是頁面跳轉)

Maven依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>
spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-core</artifactId> </dependency>

WebSecurityConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.web.http.HeaderHttpSessionIdResolver;
import org.springframework.session.web.http.HttpSessionIdResolver;

import java.util.concurrent.ConcurrentHashMap;

@Configuration
@EnableWebSecurity
@EnableSpringHttpSession
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter  {

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

    @Bean
    public SessionRepository<MapSession> sessionRepository() {
        return new MapSessionRepository(new ConcurrentHashMap<>());
    }

    @Bean
    public HttpSessionIdResolver sessionIdResolver() {
        return new HeaderHttpSessionIdResolver("X-Token");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        UserLoginFilter userLoginFilter = new UserLoginFilter(super.authenticationManager());
        // 登入成功
        userLoginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
            resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
            String token = req.getSession().getId();
            resp.getWriter().write("{\"code\": 0, \"token\": \"" + token +"\"}");
        });
        // 登入失敗
        userLoginFilter.setAuthenticationFailureHandler((req, resp, auth) -> {
            resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            resp.getWriter().write("{\"code\": 1, \"message\": \""+ auth.getMessage() + "\"}");
        });
        // 不需要鑑權的路徑
        http.authorizeRequests().antMatchers("/error", "/captchaImage").permitAll()
                .anyRequest().authenticated();
        http.logout().logoutUrl("/logout").logoutSuccessHandler( (req,resp, auth) ->{
            resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
            resp.setCharacterEncoding("UTF-8");
            resp.getWriter().println("{\"code\":0}");
        });
        http.csrf().disable();
        http.addFilterBefore(userLoginFilter, UsernamePasswordAuthenticationFilter.class);
        http.headers().cacheControl();
    }
}

UserLoginFilter

package com.yunkong.monaco.auth;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.google.code.kaptcha.Constants;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;


import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class UserLoginFilter extends AbstractAuthenticationProcessingFilter {

    private static final String USERNAME_PARAMETER = "username";

    private static final String PASSWORD_PARAMETER = "password";

    private static final String CODE_PARAMETER = "code";

    protected UserLoginFilter(AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher("/login", HttpMethod.POST.name(), true));
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException{
        if (!HttpMethod.POST.matches(request.getMethod())) {
            throw new AuthenticationServiceException("Method not supported");
        }

        String code = getCode(request);
        if (StringUtils.isBlank(code)) {
            throw new AuthenticationServiceException("驗證碼為空");
        }
        HttpSession session = request.getSession();
        String sessionCode = (String)session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
        if (!code.equals(sessionCode) && !"666".equals(code)) {
            throw new AuthenticationServiceException("驗證碼錯誤");
        }
        AuthenticationManager authenticationManager = this.getAuthenticationManager();
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(getUsername(request), getPassword(request)));
    }

    private String getUsername(HttpServletRequest request) {
        String username = request.getParameter(USERNAME_PARAMETER);
        if (username == null) {
            username = "";
        }
        return username.trim();
    }

    private String getPassword(HttpServletRequest request) {
        String password = request.getParameter(PASSWORD_PARAMETER);
        if (password == null) {
            password = "";
        }
        return password;
    }

    private String getCode(HttpServletRequest request) {
        return request.getParameter(CODE_PARAMETER);
    }
}

UserDetailsService實現類

package com.yunkong.monaco.auth;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.yunkong.monaco.entity.User;
import com.yunkong.monaco.mapper.UserMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        User user = userMapper.selectOne(Wrappers.<User>lambdaQuery()
                .eq(User::getName, name)
        );
        if (user == null) {
            throw new UsernameNotFoundException(String.format("使用者:%s,不存在", name));
        }
        return new UserDetailsImpl(user);
    }
}

UserDetails實現類

package com.yunkong.monaco.auth;

import com.yunkong.monaco.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class UserDetailsImpl implements UserDetails {

    private String password;

    private String username;

    private User user;

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

    public UserDetailsImpl(User user) {
        this.username = user.getName();
        this.password = user.getPassword();
        this.user = user;
    }

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

    @Override
    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;
    }

    public User getUser() {
        return user;
    }
}