前後端分離Spring security 從healer的token獲取Session
阿新 • • 發佈:2020-07-25
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; } }