SpringSecurity前后端分离表单登录实现

前后端分离下的表单登录

环境搭建

1. 创建User类

package com.sy.security.domain.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;

/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/9/7-20:45
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable, UserDetails {

    private String username;
    private String password;
    private String realName;
    private String roles;
    private List<GrantedAuthority> authorities;

    public User(String username, String password, String realName, String roles) {
        this.username = username;
        this.password = password;
        this.realName = realName;
        this.roles = roles;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }
    public void setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

2. 创建UserService(根据用户名去查询用户信息)

package com.sy.security.service;

import com.sy.security.domain.pojo.User;

import java.util.HashMap;

/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/9/7-20:44
 **/
public class UserService {
    private static final HashMap<String,User> map;
    //模拟数据库
    static {
        map = new HashMap<>();
        map.put("2019110231",new User("2019110231","123456","沈洋"));
        map.put("2019110211",new User("2019110211","654321","韩世凯"));
    }
    //模拟查询数据库
    public User selectUserByUsername(String username){
        return map.get(username);
    }
}

3. 前后端分离下,后端接口统一返回类

package com.sy.security.domain.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import org.springframework.http.HttpStatus;

import java.io.Serializable;
import java.util.HashMap;

/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 31-05-2021-18:05
 **/
public class Result implements Serializable {
    /**
     * 默认成功响应码
     */
    private static final Integer DEFAULT_SUCCESS_CODE = HttpStatus.OK.value();
    /**
     * 默认成功响应信息
     */
    private static final String DEFAULT_SUCCESS_MSG = "请求/处理成功!";
    /**
     * 默认失败响应码
     */
    private static final Integer DEFAULT_FAILURE_CODE = HttpStatus.INTERNAL_SERVER_ERROR.value();
    /**
     * 默认失败响应信息
     */
    private static final String DEFAULT_FAILURE_MSG = "请求/处理失败!";

    @Getter
    private Meta meta;

    @Getter
    private HashMap<String,Object> data;

    public static Result success(){
        Result result = new Result();
        result.meta = new Meta(DEFAULT_SUCCESS_CODE, DEFAULT_SUCCESS_MSG);
        return result;
    }
    public static Result success(String msg){
        Result result = new Result();
        result.meta = new Meta(DEFAULT_SUCCESS_CODE, msg);
        return result;
    }
    public static Result success(HttpStatus status,String msg){
        Result result = new Result();
        result.meta = new Meta(status.value(), msg);
        return result;
    }
    public static Result failure(){
        Result result = new Result();
        result.meta = new Meta(DEFAULT_FAILURE_CODE, DEFAULT_FAILURE_MSG);
        return result;
    }
    public static Result failure(String msg){
        Result result = new Result();
        result.meta = new Meta(DEFAULT_FAILURE_CODE, msg);
        return result;
    }
    public static Result failure(HttpStatus httpStatus, String msg){
        Result result = new Result();
        result.meta = new Meta(httpStatus.value(), msg);
        return result;
    }

    public Result build(){
        this.data=new HashMap<>();
        return this;
    }
    public Result build(HashMap<String,Object> map){
        this.data=map;
        return this;
    }
    public Result data(String name,Object o){
        if(this.data==null) data=new HashMap<>();
        this.data.put(name,o);
        return this;
    }


    @Data
    @AllArgsConstructor
    private static class Meta {

        /**
         * 处理结果代码,与 HTTP 状态响应码对应
         */
        private Integer code;

        /**
         * 处理结果信息
         */
        private String msg;
    }

}

具体实现

1. 创建WebSecurityConfig配置类

新建config包,创建我们自己的WebSecurityConfig类并且继承Security框架的WebSecurityConfigAdapter类,添加注解@EnableWebSecurity注解(该注解是有@Configuration注解的,也就是说开启后我们自定义的配置将会被Security发现并注册)

package com.sy.security.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.sql.DataSource;

/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 26-06-2021-19:37
 **/

@Slf4j
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                    //这里面的接口会开放访问权限
                    .antMatchers("/hello/test").permitAll()
                    .anyRequest().authenticated()
                //关闭跨域
                .and()
                    .csrf().disable()
                    .formLogin()
                        //未认证的用户被拦截后处理的地方(前后端分离的情况下路径是一个后端处理接口)security默认实现是
                        .loginPage("/authentication/require")
                        //处理登陆请求的接口(也就是Security要处理登录逻辑的接口)
                        .loginProcessingUrl("/login")
                        //登陆成功处理器
                        //.successHandler(authenticationSuccessHandler)
                        //登陆失败处理器
                        //.failureHandler(authenticationFailureHandler)
                        //登录请求放行
                        .permitAll()
                .and()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);


    }
    //随机盐被直接放到加密后的密码串中,验证时会自动取出里面的盐来进行匹配
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

登录逻辑

  • Security根据我们配置的拦截路径(/login)对所有请求进行拦截

  • 当遇到是登录请求时,会将请求拦截下来交给AuthenticationManager

  • manager负责管理所有能进行认证的provider,当需要认证时manager会遍历所有provider并选择合适的provider进行认证

  • provider对登录进行校验,并调用UserDetailsService的实现类去查找用户,service找到user后(根据自己逻辑实现)返回给provider

    • 由provider进行密码校验(使用BCryptPasswordEncoder验证密码)
    • 验证成功后封装成Authentication对象返回给上层manager即认证成功,并且调用SuccessHandler进行处理(根据自己逻辑实现
    • 验证失败时调用FailureHandler进行处理(根据自己逻辑实现

    Tips

    • 我们如果要实现自己的其他登录方式比如手机验证码登录、邮箱验证码登录等就需要实现自己的provider以及service等。

      因为默认的UsernamePasswordAuthenticationFilter是有进行认证的Provider的,所以这里我们就只需要实现自己的UserDetailsService、SuccessHandler、FailureHandler就可以了

    • 这里还需要注意一点的是在使用用户名密码登录的情况下,Security默认是获取post请求,请求体中的参数,所以默认情况下前端在发送登录请求时不能使用json来传递登录信息(后端也可以修改回去参数的形式,这里就不介绍了)

      在这里插入图片描述
      在这里插入图片描述

2. 实现自己的UserDetailService

package com.sy.security.service;
import com.sy.security.domain.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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;

/**
 * @author 沈洋 邮箱:1845973183@qq.com
 **/
@Service
@Slf4j
public class MyUserDetailService implements UserDetailsService {
    //注入登陆的service
    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据传入的用户名查询
        log.info("传入的用户名"+username);
        UserDetails user = buildUser(username);
        if(user==null) throw new UsernameNotFoundException("用户不存在");
        return user;
    }
    

    /**
     * 模拟获取登录用户
     * @return 返回用户实例
     */
    private UserDetails buildUser(String username){
        User user = null;
        user = userService.selectUserByUsername(username);
        if(user!=null){
//            log.info("从数据库中获取到的用户信息"+user);
            user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
            return user;
        }
        return null;
    }

}

3. 登陆成功处理

package com.sy.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sy.security.domain.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/5/13-15:21
 **/
@Component
@Slf4j
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    //登录成功后调用
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //根据配置中来选择是执行跳转,还是返回字符串
        response.setContentType("application/json;charset=UTF-8");
        //登陆成功,返回认证信息
        User user = (User) authentication.getPrincipal();
        response.getWriter().write(objectMapper.writeValueAsString(user));
    }
}

4. 登陆失败处理

package com.sy.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sy.security.domain.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/5/13-15:34
 **/
@Component
@Slf4j
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;
    //登录失败后调用
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
       
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(Result.failure(exception.getMessage())));
    }
}

注册

将成功/失败处理器注册到Security中并重启

测试

  1. 访问hello,被拦截
  2. 调用登录接口,传入正确的用户名和密码,登录成功
  3. 继续调用hello接口,正常访问

认证后如何获取到用户信息

security用户登录后默认使用session将用户信息保存起来,后续请求进来时根据sessionid将用户信息取出来放入SecurityContextHolder中,这也是为什么我们可以通过SecurityContextHolder能在各个接口中获取到用户信息的原因。

  1. SecurityContextHolder.getContext().getAuthentication();

    点进SecurityContextHolder的源码可以看到真正保存用户信息的strategy其内部是使用ThreadLocalMap来保存的,而ThreadLocalMap本身只属于当前线程,一次请求结束后会被清除。

    security用户登录后默认使用session将用户信息保存起来,后续请求进来时根据sessionid将用户信息取出来放入SecurityContextHolder中,这也是为什么我们可以通过SecurityContextHolder能在各个接口中获取到用户信息的原因。
    在这里插入图片描述

@GetMapping("/user/me")
public Object getCurrentUser(){
    return SecurityContextHolder.getContext().getAuthentication();
}
  1. 直接在方法上加入Authentication
@GetMapping("/user/me")
public Object getCurrentUser(Authentication authentication){
    return authentication;
}

前端SessionId变化问题

**问题:**因为Security默认的策略是将用户信息存放在session中,一个用户登录后会为其分配sessionid存放在浏览器的cookie中。但经常会遇到前端发送的请求每次sessionid变化的问题。对于前后端分离的项目来说前端sessionid一直变化后端就会认为是不同的用户发起的请求,无法根据sessionid获取到存放在session中的信息。从而认证失败

解决:

  • 一种解决办法是前端每次发送请求时将请求头中的sessionid设置为上一次的sessionid来保持sessionid不变化
  • 使用基于JWT的验证方式
  • List item
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shenyang1026

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值