恒久地平线

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

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

 

posted on 2020-07-25 15:35  恒久地平线  阅读(4369)  评论(0编辑  收藏  举报

腾讯微博:http://t.qq.com/zhangxh20