首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【spring-security基础】JWT前后端分离方案

【spring-security基础】JWT前后端分离方案

作者头像
master336
发布2026-06-15 19:51:39
发布2026-06-15 19:51:39
20
举报
目录

  • 依赖引入
  • 功能清单及代码详解
    • 整体配置
    • 自定义部分
      • 1. 自定义认证
      • 2. 自定义鉴权
      • 3. 自定义异常处理
      • 4. 免鉴权资源忽略
      • 5. 登出状态清理
    • JWT token处理
    • 代码参考

实现代码以【spring-security基础】基于数据库的认证方式 为前提

依赖引入

jwt需要引入对应工具包,用于维护token

代码语言:javascript
复制
<dependency>
   <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

功能清单及代码详解

    1. 自定义认证
    1. 自定义鉴权
    1. 自定义异常处理
    1. 免鉴权资源忽略
    1. 登出状态清理

整体配置

这部分配置了自定义异常处理及过滤器

代码语言:javascript
复制
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 未授权访问
                .exceptionHandling()
                    // 认证失败
                    .authenticationEntryPoint(myUnAuthEntryPoint)
                    // 鉴权失败
                    .defaultAccessDeniedHandlerFor(new MyAccessDeniedHandler(),new AntPathRequestMatcher("/**"))
                .and()
                // 关闭csrf检查,此项检查开启时可能导致过滤链被提前终止
                .csrf().disable()
//                // 资源权限
                .authorizeRequests().anyRequest().authenticated()
                //关闭basic验证
                .and().httpBasic().disable()
//                // 登录设置 (自定义认证过滤器后失效,如本例中已经定义MyTokenLoginFilter)
                .formLogin().loginProcessingUrl("/noauth/sublogin")
                .and()
//                // 退出登录设置
                .logout().logoutUrl("/noauth/logout").addLogoutHandler(myLogoutHandler)
                .and()
//                // 认证处理器
                .addFilter(new MyTokenLoginFilter(authenticationManager(), myTokenConfig))
//                // 授权处理器
                        .addFilter(new MyTokenAuthFilter(authenticationManager(),myTokenConfig));
    }

配置忽略鉴权路径

代码语言:javascript
复制
@Override
    public void configure(WebSecurity webSecurity) throws Exception {
        // 忽略鉴权及认证配置,配置此项的地址将不会被security过滤连过滤(包括认证、登出等)
        webSecurity.ignoring().antMatchers("/auth/**");
    }

配置密码处理器及用户查询服务

代码语言:javascript
复制
 @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        // 设置用户查询服务及密码处理器
       authenticationManagerBuilder.userDetailsService(userDetailService).passwordEncoder(myPasswordEncoder);
        // 当手动注入AuthenticationProvider时,需要做如下声明
        //authenticationManagerBuilder.authenticationProvider(daoAuthenticationProvider());

    }

自定义部分

1. 自定义认证

自定义认证过滤器主要解决认证信息的获取及处理,比如当前后端分离项目中,密码或者用户名使用加密等手段传递时,需要在这里定义自己的解密方法,以供security完成认证。 实现自定义认证只需要继承AbstractAuthenticationProcessingFilter (或其子类)即可,

覆盖Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 方法,用于处理前端请求过来的认证信息,比如以application/json 方式请求过来的数据,或加密数据,在这个方法内,需要根据将数据处理成密码处理器所能完成认证的格式(比如简单的equals)。

覆盖void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) 方法,完成认证成功是的token生成及缓存工作。(这里用本地缓存模拟redis效果)

覆盖void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) 用于处理在认证失败时的响应信息处理

代码语言:javascript
复制
package cn.com.demo.filter;

import cn.com.demo.common.vo.BaseRepVo;
import cn.com.demo.config.MyUser;
import cn.com.demo.config.MyTokenConfig;
import cn.com.demo.entity.UserInfo;
import cn.com.demo.util.LocalCache;
import cn.com.demo.util.ResponseUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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

public class MyTokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;
    MyTokenConfig myTokenConfig;

    public MyTokenLoginFilter(AuthenticationManager authenticationManager, MyTokenConfig myTokenConfig) {
        this.authenticationManager = authenticationManager;
        this.myTokenConfig = myTokenConfig;
        // 设置登录地址及请求方式 (默认:/login POST)
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/noauth/sublogin", "POST"));
    }

    // 获取登录信息
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            // 修改获取用户名密码的过程 ,对于前后端分离项目中密码加密存储时,在这一步进行密码解密 以保证密码能正确处理密码信息
            UserInfo userInfo = new ObjectMapper().readValue(request.getInputStream(), UserInfo.class);
            // 提交security处理认证过程
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userInfo.getUsername(), userInfo.getPassword(), new ArrayList<GrantedAuthority>()));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("获取认证信息异常");
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        System.out.println("登录成功");
        // 获取认证成功信息
        MyUser user = (MyUser) authResult.getPrincipal();
        // 缓存权限信息
        LocalCache.put(user.getUsername(),user.getAuthorities());
        // 生成token
        String token = myTokenConfig.getToken(user.getUsername());
        HashMap<String,String> data = new HashMap<>();
        data.put("token",token);
        ResponseUtil.out(response,new BaseRepVo(data));
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        System.out.println("登录失败");
        ResponseUtil.error(response,failed.getMessage());
    }
}

自定义用户服务,用于根据用户名从数据库(或其他鉴权接口)中查询用户及权限信息,以用于security的鉴权。默认情况下UsernameNotFoundException异常会被转换成BadCredentialsException,如需处理,需要手动配置authenticationManagerBuilder.authenticationProvider(provider)进行覆盖(demo代码中已实现)

代码语言:javascript
复制
package cn.com.demo.service;

import cn.com.demo.config.MyUser;
import cn.com.demo.dao.RolePermissionDao;
import cn.com.demo.dao.UserInfoDao;
import cn.com.demo.entity.UserInfo;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private UserInfoDao userInfoDao;
    @Autowired
    private RolePermissionDao rolePermissionDao;
    /**
     * 按需决定是否需要引入密码加密
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
     */
    @Override
    public UserDetails loadUserByUsername(String usename) throws UsernameNotFoundException {
        // 查询数据库
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username",usename);
        UserInfo userInfo = userInfoDao.selectOne(queryWrapper);
        if (userInfo ==null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // 获取用户角色
        List<String> roles = rolePermissionDao.getRuleByUserId(userInfo.getId());
        // 获取用户权限(url)
        List<String> per = rolePermissionDao.getPermissByUserId(userInfo.getId());
        for (String role : roles) {
            per.add("ROLE_"+role);
        }
        List<GrantedAuthority> auths = new ArrayList<>();
        for (String s : per) {
            GrantedAuthority e = new SimpleGrantedAuthority(s);
            auths.add(e);
        }
        // 返回认证主体UserDetails
        return new MyUser(userInfo.getUsername(),userInfo.getPassword(),auths);
        //使用非自定义的密码,可能需要进行加密后再放置进认证主体密码属性中去
        // return new User(userInfo.getUsername(),bCryptPasswordEncoder.encode(userInfo.getPassword()),auths);
    }
}

自定义密码处理器用于密码加密(如存储到数据库前的加密)及匹配鉴权

代码语言:javascript
复制
package cn.com.demo.config;

import cn.com.demo.util.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 自定义密码加密方式
 */
@Component
public class MyPasswordEncoder implements PasswordEncoder {
    /**
     * 加密密码
     * @param rawPassword 原始密码
     * @return
     */
    @Override
    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }

    /**
     * 密码解密
     * @param rawPassword 原始密码(页面提交过来的)
     * @param encodedPassword 已存储的密码(数据库等)
     * @return
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equalsIgnoreCase(MD5.encrypt(rawPassword.toString()));
    }
}
2. 自定义鉴权

自定义鉴权主要收集用户的权限,提交security进行鉴权。 jwt实现时,通过访问request的header部分查询token,并进行有效期检查及获取其权限,这部分自定义可以实现jwt token有效期的检查,同时也可以完成手动退出时token仍在有效期可继续使用的问题。

代码语言:javascript
复制
package cn.com.demo.filter;

import cn.com.demo.config.MyTokenConfig;
import cn.com.demo.util.LocalCache;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

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

public class MyTokenAuthFilter extends BasicAuthenticationFilter {
    MyTokenConfig myTokenConfig;
    public MyTokenAuthFilter(AuthenticationManager authenticationManager, MyTokenConfig myTokenConfig) {
        super(authenticationManager);
        this.myTokenConfig = myTokenConfig;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 获取认证成功的用户授权信息
        String token = request.getHeader(myTokenConfig.getTokenConfigProperties().getHeadName());
        if(!StringUtils.isEmpty(token)) {
            String userName = myTokenConfig.getUserName(token);
            List<GrantedAuthority> authorities = (List<GrantedAuthority>) LocalCache.get(userName);
            if(authorities != null) {
                // 放置权限到security上下文中
                UsernamePasswordAuthenticationToken auths = new UsernamePasswordAuthenticationToken(userName, token, authorities);
                SecurityContextHolder.getContext().setAuthentication(auths);
            }
        }
        // 过滤链需要继续向下
        chain.doFilter(request,response);
    }

    @Override
    protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException {
        System.out.println("鉴权通过。。。。");
    }

    @Override
    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
        System.out.println("鉴权失败");
    }
}

需要注意的是,虽然覆写了onSuccessfulAuthentication、onUnsuccessfulAuthentication但并没有在执行链(doFilter\doFilterInternal)中完整处理,如需处理可参考其父类进行处理(因为这两个方法为BasicAuthenticationFilter 增强的两个方法,而非标准处理链上的方法)。

3. 自定义异常处理

这部分处理包括两部分;认证过程中异常的处理及权限不足时的处理,处理这部分主要用于在用户没有权限时覆盖默认的page转发及HTTP状态码

  1. 通过认证入口点(AuthenticationEntryPoint)的实现,处理认证过程中的异常处理
代码语言:javascript
复制
public class MyUnAuthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseUtil.out(response, BaseRepVo.UNAUTH);
    }
}
  1. 通过访问限制(AccessDeniedHandler)接口的实现,可根据url配置不通的处理器
代码语言:javascript
复制
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseUtil.out(response, BaseRepVo.FORBIDDEN);
    }
}
4. 免鉴权资源忽略

通过重写configure(WebSecurity webSecurity) 方法,配置其ignoring().antMatchers(String… antPatterns)方法 配置忽略过滤的资源,需要注意的是这里配置的资源将不会完整执行security的过滤器链,比如如果将认证过滤器(自定义)的url配置到这里,你的自定义认证过滤器也就不会生效了。 具体代码见整体配置。

5. 登出状态清理

通过实现LogoutHandler 接口完成退出登录的清理工作,需要注意的是,security有自己的会话维护(实现了SecurityContextHolderStrategy接口)。

代码语言:javascript
复制
public class MyLogoutHandler implements LogoutHandler {
    @Autowired
    private MyTokenConfig myTokenConfig;
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String token = request.getHeader(myTokenConfig.getTokenConfigProperties().getHeadName());
        if(!StringUtils.isEmpty(token)) {
            // token清理工作,如在redis中删除token,防止未过期的token继续使用
            String userName = myTokenConfig.getUserName(token);
            LocalCache.remove(userName);
        }
        ResponseUtil.ok(response);
    }
}

JWT token处理

token的处理主要包括基于用户名(用户信息)生成token,及对token的解析(这里仅在token中放置了用户名信息)

代码语言:javascript
复制
package cn.com.demo.config;

import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.compression.GzipCompressionCodec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class MyTokenConfig {
    @Autowired
    TokenConfigProperties tokenConfigProperties;
    public String getToken(String userName) {
        return Jwts.builder()
                .setSubject(userName)
                .setExpiration(new Date(System.currentTimeMillis() + tokenConfigProperties.getExpireTime()*1000))
                .signWith(SignatureAlgorithm.HS512,tokenConfigProperties.getSignKey())
                .compressWith(new GzipCompressionCodec()).compact();
    }
    public String getUserName(String token) {
        try {
            return Jwts.parser().setSigningKey(tokenConfigProperties.getSignKey()).parseClaimsJws(token).getBody().getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("token 解析异常");
        }
        return null;
    }
    public TokenConfigProperties getTokenConfigProperties(){
        return tokenConfigProperties;
    }
}

代码参考

demo代码地址 demo

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-06-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • 依赖引入
  • 功能清单及代码详解
    • 整体配置
    • 自定义部分
      • 1. 自定义认证
      • 2. 自定义鉴权
      • 3. 自定义异常处理
      • 4. 免鉴权资源忽略
      • 5. 登出状态清理
    • JWT token处理
    • 代码参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档