SpringBoot+SpringSecurity+JWT实现无状态登录认证

image.png

源码地址

https://github.com/pwzos/SpringSecurityForJWT

1、导包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
    </parent>
    <groupId>com.pingwazi</groupId>
    <artifactId>SpringSecurityForJWT</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <!-- 引入web模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- spring security需要的包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- jjwt需要的包 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- hutool工具 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.10</version>
        </dependency>
    </dependencies>
</project>

2、编写JwtUtils工具包

主要用于生成jwt token串和获取token串中的载荷值。现在只有两个最核心的方法,当然你可以吧这个工具包扩展得更加强大。

/**
 * @author pingwazi
 * @description jwt 的工具包
 */
public class JwtUtils {
    private static final String jwtClaimKey="tokenObj-key";
    private static final String jwtSecretKey="jwtSecret-Key";

    /**
     * 生成jwt的token串
     * @param value
     * @return
     */
    public static String createJwtToken(String value)
    {
        HashMap<String,Object> claims=new HashMap<>();
        claims.put(jwtClaimKey,value);
        Calendar calendar=Calendar.getInstance();
        calendar.add(Calendar.HOUR_OF_DAY,24);//当前时间添加24是小时,即token在24小时后过期
        return Jwts.builder()
                .setClaims(claims)//设置载荷部分
                .setExpiration(calendar.getTime())//设置过期时间
                .signWith(SignatureAlgorithm.HS512, jwtSecretKey)//设置加密算法
                .compact();
    }

    /**
     * 从jwttoken串中获取载荷值
     * @param tokenStr
     * @return
     */
    public static String getJwtTokenClaimValue(String tokenStr)
    {
        String result=null;
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(jwtSecretKey)
                    .parseClaimsJws(tokenStr)
                    .getBody();

            if(claims.getExpiration().compareTo(Calendar.getInstance().getTime())>0)
            {
                //token未过期
                result=claims.get(jwtClaimKey,String.class);
            }
        } catch (Exception ex) {
            System.out.println(ex);
        }
        return result;
    }
}

3、创建UserEntity

/**
 * @author pingwazi
 * @description 用户信息实体
 */
public class UserEntity {
    private  String userName;
    private String password;
    private List<String> authorities;

    //======下面的getter、setter、toString代码都可以不手动编写,只是使用编辑工具的自动生成即可======
 

4、编写UserService接口及其实现类

/**
 * @author pingwazi
 * @description 用户的业务方法
 */
public interface UserService {
     UserEntity getByUserName(String userName);
     String login(String userName,String password);
}
//====下面是实现类====
/**
 * @author pingwazi
 * @description 用户信息实现类
 */
@Service
public class UserServiceImpl  implements UserService {
    /**
     * 通过用户名获取用户信息
     * @param userName
     * @return
     */
    @Override
    public UserEntity getByUserName(String userName) {
        //这里应该要访问存储介质获取到用户信息的,但是这些步骤都是十分常规的操作,因此这里跳过,直接模拟了访问数据
        List<String> authorities=new ArrayList<>();
        authorities.add("ALL");
        UserEntity user=new UserEntity("pingwazi","123",authorities);
        return user;
    }

    /**
     * 用户登录,如果账号密码比对成功,就生成一个token串返回给前端
     * @param userName
     * @param password
     * @return
     */
    @Override
    public String login(String userName, String password) {
        //这里应该要访问存储介质获取到用户信息的,但是这些步骤都是十分常规的操作,因此这里跳过,直接模拟了访问数据
        if("pingwazi".equals(userName) && "123".equals(password))
        {
           return JwtUtils.createJwtToken(userName);
        }
        return "";
    }
}

5、编写两个自定义的错误处理器

这两个类中的方法都是由spring security触发某种错误时才会调用的,比如说没有进行认证或者访问了没有权限的接口。

/**
 * @author pingwazi
 * @description 访问没有授权时的处理器
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");
        response.setStatus(200);
        response.getWriter().println("您当前访问未授权");//这里返回一个字符串,可以吧一个对象序列化之后再返回。
        response.getWriter().flush();
    }
}



/**
 * @author pingwazi
 * @description 认证信息失效(未认证或者认证信息过期)处理器
 */
@Component
public class RestfulAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");
        response.setStatus(200);
        response.getWriter().println("您当前的认证信息无效");//这里的返回信息是一个字符串,也就是说可以是吧一个对象序列化再放回
        response.getWriter().flush();
    }
}

6、自定义JWT认证的核心Filter

虽然这里的实现是给予JWT进行实现的,但如果你明白了其原理,实际上你可以将其改造为任何方式的方式

/**
 * @author pingwazi
 * @description
 */
@Component
public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private UserService userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        //当前上下文中不存在认证信息
        //尝试获取token (token不一定存放在header中,比如也可以当做请求参数进行传递)
        //尝试从token中解析对象 (token中可以存放任何信息)
        //尝试从根据存放在token的信息去找对应的用户信息
        //用户找到用户信息信息 就在当前的认证上下文中进行设置,确保后续的filter能够检测到认证通过
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            String tokenStr = request.getHeader("token");
            if (StrUtil.isNotBlank(tokenStr)) {
                String tokenObj = JwtUtils.getJwtTokenClaimValue(tokenStr);
                if (StrUtil.isNotBlank(tokenObj)) {
                    UserEntity user = userService.getByUserName(tokenObj);
                    if (user != null) {
                        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                        if (user.getAuthorities() != null && user.getAuthorities().size() > 0) {
                            authorities = user.getAuthorities().stream().map(a -> new SimpleGrantedAuthority(a)).collect(Collectors.toList());
                        }
                        //设置当前上下文的认证信息
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(tokenObj, "", authorities);
                        authentication.setDetails(user);
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }

            }
        }
        //调用下一个过滤器
        chain.doFilter(request, response);
    }
}

7、编写SpringSecurity的配置类

配置类中使用了我们自定义的filter和两个错误处理器,其中配置那些接口允许访问,那些接口不能访问也是十分方便的。

/**
 * @author pingwazi
 * @description
 */
@Configuration
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestfulAuthenticationEntryPoint restfulAuthenticationEntryPoint;
    @Autowired
    private JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//不使用防跨站攻击
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不使用session
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/swagger-resources/**",
                        "/v2/api-docs/**").permitAll()//允许静态资源无授权访问
                .and()
                .authorizeRequests().antMatchers("/admin/login", "/admin/register").permitAll()//允许登录接口、注册接口访问
                .and()
                .authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll()//配置跨域的option请求,跨域请求之前都会进行一次option请求
                .and()
                .authorizeRequests().anyRequest().authenticated();//其他没有配置的请求都需要身份认证
        http.headers().cacheControl();//http的cache控制,如下这句代码会禁用cache
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//添加JWT身份认证的filter
        //添加自定义未授权的处理器
        http.exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler);
        //添加自定义未登录的处理器
        http.exceptionHandling().authenticationEntryPoint(restfulAuthenticationEntryPoint);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350