[多登录页]Spring boot集成Spring security及JWT实现多页面(多种登录方式)前后端分离登录鉴权

[多登录页]Spring boot集成Spring security及JWT实现多页面(多种登录方式)前后端分离登录鉴权


前言

我的这篇文章详细描述了如何使用Spring boot集成Spring security及JWT实现单一登录页面前后端分离登录鉴权。但是在服务设计中,经常有多端登录的需求。比如一套系统一般分为后台管理系统,和前台系统。更细致区分,可能前台还分为不同登录端,比如移动端和PC端。这时,不同的系统就需要不同的登录鉴权体系。比如admin的登录鉴权无论是颁发的token还是使用的用户存储表,都与user前台的数据不同。这就要求我们在使用Spring security的时候,具备提供多套登录鉴权认证体系的能力。如果对此问题比较感兴趣的兄弟,请继续看下文

文中整套demo的构建目标如下:

  1. 前后端分离系统,没有使用thymeleaf等模板引擎,所以只提供api接口,没有做页面跳转。
  2. admin登录form提交位置为/admin/login,user登录form提交位置为/user/login。
  3. admin和user通过不同表存取用户信息。

最后说一句啊,由于现在基本都是微服务架构了,微服务下的服务认证体系,最好还是用Oauth2.0搞认证鉴权中心才是正途,然后Oauth2.0如果要实现一个认证中心多个类型的账户认证,这种方案也可以用

基础配置(与单一页面配置方案相同)

依赖配置

    <dependencies>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--JWT(Json Web Token)登录支持-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
    </dependencies>

  <!--版本管理-->
    <dependencyManagement>
        <dependencies>
            <!--SpringBoot-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.7.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

yml配置

# JWT
jwt:
  # JWT存储的请求头
  tokenHeader: Authorization
  # JWT 解密加密使用的密钥
  salt: web-secret
  # JWT的超期限时间(30*60*24)
  expiration: 604800
  # JWT 负载中拿到开头
  tokenHead: Bearer

JWT配置

引入JWT工具类

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken生成的工具类
 */
@Component
public class JwtTokenUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    @Value("${jwt.salt}")
    private String salt;
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 根据claims生成token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims) // 设置负载
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) // 过期时间,当前时间 + 过期时间
                .signWith(SignatureAlgorithm.HS512, salt) // 签发算法及秘钥
                .compact();
    }

    /**
     * 根据userDetails生成token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 验证token是否还有效
     *
     * @param token       客户端传入的token
     * @param userDetails 从数据库中查询出来的用户信息
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 从token中获取claims
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(salt)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}",token);
        }
        return claims;
    }

    /**
     * 从token中获取loginname
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username =  claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 判断token是否已经失效
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 判断token是否可以被刷新
     */
    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     * 刷新token
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
}

至此以上三部分都与单一页面认证无异

核心配置

原理

由于spring security没有单独考虑过多页面登录方案。比如,单一页面登录逻辑的实现,是通过自定义serDetailsService方案实现的,但是这种方案自定义化程度低,相当于只把真实用户数据的读取自定义能力开放给了使用者,因此这种方式不适合用于改写多端登录。所以如果要实现多页面登录,要自定义一套登录逻辑,本文使用的是Filter+Manager+Provider+Token的方案实现,涉及到部分源码的改写,但是不难,大家跟着注释能走下来。简单描述下这几个东西的作用:

  1. Filter负责拦截请求,并调用Manager的authenticate方法完成认证

    spring security本身就是通过一系列filter实现登录鉴权的拦截认证的。所以要新加一套自定义的登录认证体系,首先要加一个Filter做为拦截入口

  2. Manager负责管理多个Provider,并选择合适的Provider进行认证。Manager的authenticate方法实际上是调用Provider中authenticate方法执行的

    这里不做详细解释,详细源码解析在这里

  3. Provider负责具体authenticate逻辑的处理,检验账号密码等操作。

    Provider下面有许多具体实现类,有一个专门封装好了的实现类叫AbstractUserDetailsAuthenticationProvider,可以基于此类改写登录认证逻辑。

  4. Token是认证信息,包含账号密码。

    filter、manager、provider之间一直流转的账号信息封装类,是UsernamePasswordAuthenticationToken,用于封装用于认证的用户信息。这里和UserDetails做个区分,UsernamePasswordAuthenticationToken中一直封装的是被认证信息,而UserDetails中封装的则是从用户数据库中读取的,真实用户信息。

关于spring security中各个filter的介绍,建议参考这篇文章

废话不多说,开整

登录认证流程

普通request -> 全部拦截到JwtAuthenticationTokenFilter完成Token登录鉴权,如果没有携带Token,后续有两种可能

① 如果uri是/admin/login或/user/login,将请求拦截到CustomFilter进行账号密码认证。

② 如果uri不是/admin/login或/user/login,请求放行,进行后续处理。

配置controller,用于测试效果

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TestController {

    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        return "hello spring sercurity";
    }

    @ResponseBody
    @GetMapping("/admin/hello")
    public String adminHello(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "hello admin:" + authentication.getPrincipal();
    }

    @ResponseBody
    @GetMapping("/user/hello")
    public String userHello(){
        return "hello user";
    }
}

配置MyUserService用于模拟真实用户数据存取

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 模拟不同类型用户数据
 */
@Component
public class MyUserService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 模拟admin数据库获取数据
     * @return
     */
    public UserDetails adminUserDetails(){
        return User.withUsername("admin")
                .password(passwordEncoder.encode("123456"))
                .roles("ADMIN")
                .build();};

    /**
     * 模拟user数据库获取数据
     * @return
     */
    public UserDetails userUserDetails() {
        return User.withUsername("user")
                .password(passwordEncoder.encode("654321"))
                .roles("USER")
                .build();
    }
}

配置JwtAuthenticationTokenFilter进行带token request鉴权认证

import com.example.testproject.util.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

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

/**
 * JWT过滤器
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    private MyUserService myUserService;

    /**
     * 完成带token的request的解析和鉴权
     *
     * @param request
     * @param response
     * @param chain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        String authHeader = request.getHeader(this.tokenHeader);

        // token非空的情况,解析出username
        if (StringUtils.hasLength(authHeader) && authHeader.startsWith(this.tokenHead)) {
            // token去头
            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
            // 解析username
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("user token checking:{}", username);

            // 验证token合法性,SecurityContextHolder.getContext().getAuthentication()用于验证此request是否在前序已经通过了验证
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                // 根据login uri拿到用哪张表鉴权,获取对比对象
                UserDetails userDetails = null;
                if (request.getRequestURI().startsWith("/admin/")){
                    userDetails = myUserService.adminUserDetails();
                }else if (request.getRequestURI().startsWith("/user/")){
                    userDetails = myUserService.userUserDetails();
                }else{
                    userDetails = myUserService.userUserDetails();
                }

                // 使用jwt signature验证token真实性
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    // 验证成功,组装UsernamePasswordAuthenticationToken对象,放入SecurityContextHolder中,表示鉴权成功
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    LOGGER.info("user token success:{}", username);
                } else {
                    // token验证不成功
                    LOGGER.info("user token failed:{}", username);
                    throw new BadCredentialsException("Bad credentials");
                }
            } else {
                // username为空,或前序filter已经验证通过此request
                LOGGER.info("user token pass:{}", username);
            }
        }else{
            // token为空的情况,交给后续CustomFilter等其他filter,做账号密码处理
        }
        chain.doFilter(request, response);
    }
}

多页面登录的JwtAuthenticationTokenFilter与单页面登录的配置有所差别,主要差别在于解析token,并根据token进行登录认证上。单页面登录场景下,UserDetails userDetails = userDetailsService.loadUserByUsername(username); 。多页面场景下,userDetails 要根据request的uri去判断从哪个表取值

配置Filter 进行不带token request(基于账号密码)鉴权认证

import org.springframework.lang.Nullable;
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 org.springframework.util.Assert;

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

/**
 * 全盘照抄UsernamePasswordAuthenticationFilter,只改了2处
 * 1. 构造方法允许自定义url
 * 2. setDetails改为向UsernamePasswordAuthenticationToken中存储request数据,用于provider中根据uri确定查哪个表
 */
public class CustomFilter extends AbstractAuthenticationProcessingFilter {

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private static final AntPathRequestMatcher ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/user/login",
            "POST");

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

    private boolean postOnly = true;

    public CustomFilter(String path) {
        super(new AntPathRequestMatcher(path, "POST"));
    }

    public CustomFilter(String path, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(path, "POST"), authenticationManager);
    }


    /**  以下为全盘照抄 UsernamePasswordAuthenticationFilter **/

    
    /**
     * detail放入request
     * @param request
     * @param authRequest
     */
    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(request);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * Enables subclasses to override the composition of the password, such as by
     * including additional values and a separator.
     * <p>
     * This might be used for example if a postcode/zipcode was required in addition to
     * the password. A delimiter such as a pipe (|) should be used to separate the
     * password and extended value(s). The <code>AuthenticationDao</code> will need to
     * generate the expected password in a corresponding manner.
     * </p>
     * @param request so that request attributes can be retrieved
     * @return the password that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    /**
     * Enables subclasses to override the composition of the username, such as by
     * including additional values and a separator.
     * @param request so that request attributes can be retrieved
     * @return the username that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    /**
     * Sets the parameter name which will be used to obtain the username from the login
     * request.
     * @param usernameParameter the parameter name. Defaults to "username".
     */
    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    /**
     * Sets the parameter name which will be used to obtain the password from the login
     * request..
     * @param passwordParameter the parameter name. Defaults to "password".
     */
    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    /**
     * Defines whether only HTTP POST requests will be allowed by this filter. If set to
     * true, and an authentication request is received which is not a POST request, an
     * exception will be raised immediately and authentication will not be attempted. The
     * <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
     * authentication.
     * <p>
     * Defaults to <tt>true</tt> but may be overridden by subclasses.
     */
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }

}

原计划使用UsernamePasswordAuthenticationFilter做修改,但是发现UsernamePasswordAuthenticationFilter的ANT_PATH_REQUEST_MATCHER变量给了个final值,说明作者不希望有人继承修改这个类,因此,向上找到了AbstractAuthenticationProcessingFilter这个类。但是由于此次也是要实现基于账号密码登录的认证,为了防止部分属性遗漏,还是copy了UsernamePasswordAuthenticationFilter类的实现,只改了2个地方

  1. 构造方法允许自定义url
  2. setDetails改为向UsernamePasswordAuthenticationToken中存储request数据,用于provider中根据uri确定查哪个表

配置MultiUserProvider用于实现具体用户认证逻辑

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Component
public class MultiUserProvider extends AbstractUserDetailsAuthenticationProvider {

    @Autowired
    private MyUserService myUserService;

    /**
     * 存储用户资源获取
     * @param username
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 获取filter中存放的request
        HttpServletRequest request = (HttpServletRequest) authentication.getDetails();
        String uri = request.getRequestURI();
        // 根据路径匹配具体使用哪套账号密码
        UserDetails userDetails = null;
        if (uri.startsWith("/admin")){
            userDetails = myUserService.adminUserDetails();
        }else if(uri.startsWith("/user")){
            userDetails = myUserService.userUserDetails();
        }
        return userDetails;
    }

    /**
     * 凭据检查
     * 参考https://blog.csdn.net/dengdeying/article/details/103678030 源码详解
     * @param userDetails
     * @param authentication
     * @throws AuthenticationException
     */
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        boolean username = authentication.getPrincipal().equals(userDetails.getUsername());
        boolean passowrd = passwordEncoder.matches((String) authentication.getCredentials(),userDetails.getPassword());
        if (!username || !passowrd){
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
}

配置SecurityConfig,实现spring security全局控制

import cn.hutool.json.JSONUtil;
import com.example.testproject.common.Result;
import com.example.testproject.common.ResultGenerator;
import com.example.testproject.component.JwtAuthenticationTokenFilter;
import com.example.testproject.component.CustomFilter;
import com.example.testproject.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

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

/**
 * Spring Security配置
 */
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    private AuthenticationProvider MultiUserProvider;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    /**
     * 全局配置:路径、csrf、session等功能启停
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // admin页面拦截配置
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasRole("USER")
            .anyRequest()// 除上面外的所有请求全部需要鉴权认证
            .authenticated()
            // 登录配置
            .and()
            .formLogin().permitAll()
            // 登出配置
            .and()
            .logout()
            .logoutUrl("/admin/logout")
            .permitAll()
            // 无权限访问和访问失败
            .and()
            .exceptionHandling()
            .accessDeniedHandler(new CustomAccessDeniedHandler())
            .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
            // 自定义权限拦截器JWT过滤器
            .and()
            .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
            .addFilterAfter(customFilter("/admin/login"), UsernamePasswordAuthenticationFilter.class)
            .addFilterAt(customFilter("/user/login"), UsernamePasswordAuthenticationFilter.class);

        // 跨域拦截放开
        http.csrf().disable();
        // 禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    /**
     * 注册编解码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 处理成功配置
     */
    class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            // 获取用户信息
            User user = (User) authentication.getPrincipal();
            // 生成token
            String token = jwtTokenUtil.generateToken(user);
            // 封装token
            response.setHeader("token", token);
            response.setHeader("Access-Control-Expose-Headers", "token");//暴露自定义回复头
            Result result = ResultGenerator.genSuccessResult();
            result.setData(token);
            // 转JSON
            String s = JSONUtil.toJsonStr(result);
            // 输出respon
            response.setContentType("text/html; charset=UTF-8");
            PrintWriter writer = response.getWriter();
            writer.println(s);
            writer.flush();
            writer.close();
        }

    }

    /**
     * 处理失败配置
     */
    class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            // 获取错误信息
            Result result = ResultGenerator.genFailResult(exception.getMessage());
            // 转JSON
            String s = JSONUtil.toJsonStr(result);
            // 输出respon
            response.setContentType("text/html; charset=UTF-8");
            PrintWriter writer = response.getWriter();
            writer.println(s);
            writer.flush();
            writer.close();
        }

    }

    /**
     * 权限不足拒绝访问配置
     */
    class CustomAccessDeniedHandler implements AccessDeniedHandler{
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            // 获取错误信息
            Result result = ResultGenerator.genFailResult(accessDeniedException.getMessage());
            // 转JSON
            String s = JSONUtil.toJsonStr(result);
            // 输出respon
            response.setContentType("text/html; charset=UTF-8");
            PrintWriter writer = response.getWriter();
            writer.println(s);
            writer.flush();
            writer.close();
        }
    }

    /**
     * 未通过认证访问配置
     */
    class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            // 获取错误信息
            Result result = ResultGenerator.genFailResult(authException.getMessage());
            // 转JSON
            String s = JSONUtil.toJsonStr(result);
            // 输出respon
            response.setContentType("text/html; charset=UTF-8");
            PrintWriter writer = response.getWriter();
            writer.println(s);
            writer.flush();
            writer.close();
        }
    }

    /**
     * uri过滤器,根据不同uri匹配不同规则
     * @param path
     * @return
     * @throws Exception
     */
    private CustomFilter customFilter(String path) throws Exception{
        CustomFilter adminFilter = new CustomFilter(path);
        adminFilter.setAuthenticationManager(authenticationManager());
        //登录成功后跳转
        adminFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
        adminFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
        return adminFilter;
    }

    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
        return new JwtAuthenticationTokenFilter();
    };


    //核心:配置管理器
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //在管理器中添加provider
        auth.authenticationProvider(MultiUserProvider).build();
    }
}

这里面的代码和单页面登录逻辑差距不大,最重要的就三行代码。这里要注意jwtAuthenticationTokenFilter一定要放在所有认证filter的最前面,因为带token的请求一定要先过滤掉,其次才是admin和user的请求

.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(customFilter("/admin/login"), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(customFilter("/user/login"), UsernamePasswordAuthenticationFilter.class);

配置结果

可实现最终目标,使用postman发送请求,可观测到/admin/login和/user/login得到的登录结果token不同,各自的token只能够访问各自路径下的资源。比如/admin/login得到的token只能用于访问/admin/l下的资源,/user/login得到的token能用于访问/**下的资源,

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Spring Boot和Vue.js是两个非常流行的技术栈,可以非常好地实现前后端分离的开发模式。SecurityJWT是两个很好的工具,可以帮助我们实现安全的登录和授权机制。 以下是实现Spring Boot和Vue.js前后端分离的步骤: 1.创建Spring Boot工程 首先,我们需要创建一个Spring Boot工程,可以使用Spring Initializr来生成一个基本的Maven项目,添加所需的依赖项,包括Spring SecurityJWT。 2.配置Spring SecuritySpring Security中,我们需要定义一个安全配置类,该类将定义我们的安全策略和JWT的配置。在这里,我们可以使用注解来定义我们的安全策略,如@PreAuthorize和@Secured。 3.实现JWT JWT是一种基于令牌的身份验证机制,它使用JSON Web Token来传递安全信息。在我们的应用程序中,我们需要实现JWT的生成和验证机制,以便我们可以安全地登录和授权。 4.配置Vue.js 在Vue.js中,我们需要创建一个Vue.js项目,并使用Vue CLI来安装和配置我们的项目。我们需要使用Vue Router来定义我们的路由,并使用Axios来发送HTTP请求。 5.实现登录和授权 最后,我们需要实现登录和授权机制,以便用户可以安全地登录和访问我们的应用程序。在Vue.js中,我们可以使用Vue Router和Axios来发送HTTP请求,并在Spring Boot中使用JWT来验证用户身份。 总结 以上是实现Spring Boot和Vue.js前后端分离的步骤,我们可以使用SecurityJWT实现安全的登录和授权机制。这种开发模式可以让我们更好地实现前后端分离,提高我们的开发效率和应用程序的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值