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
HeaderHttpSessionIdResolver,从Header获取SessionId,代替从Cookie获取。只不过这时候称它为Token。
MapSessionRepository,把Session保存Map,如果是分布式环境,这个可以改成保存在Redis
UserLoginFilter,登录校验,过滤顺序是在UsernamePasswordAuthenticationFilter之前
设置登录成功、登录失败后返回给客户端的json
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) ) { 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实现类
根据username到数据库里获取密码,并封装成UserDetails对象返回。
该类在UserLoginFilter过滤执行authenticationManager.authenticate()方法时,会被调用,并且校验密码是否正确。
这个也是为什么不在UserLoginFilter里做密码校验。
另外,数据里的密码保存的是密文,而登录输入的密码是明文,所以需要对密码进行加密,才能最终匹配。
在WebSecurityConfig配置的PasswordEncoder是NoOpPasswordEncoder是没有做任何加密的。实际使用应该设成其他PasswordEncoder
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实现类
用户登录后,一般都会放一些用户的信息在Session里
Spring Security保存UserDetails对象在Session
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; } }
获取Session中的用户信息
public class HttpSessionUtil { public static User getUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); if (userDetails != null) { return userDetails.getUser(); } return null; } }