草庐IT

厉害!我带的实习生仅用四步就整合好SpringSecurity+JWT实现登录认证!

沉默王二 2023-03-28 原文

小二是新来的实习生,作为技术 leader,我还是很负责任的,有什么锅都想甩给他,啊,不,一不小心怎么把心里话全说出来了呢?重来!

小二是新来的实习生,作为技术 leader,我还是很负责任的,有什么好事都想着他,这不,我就安排了一个整合SpringSecurity+JWT实现登录认证的小任务交,没想到,他仅用四步就搞定了,这让我感觉倍有面。

一、关于 SpringSecurity

在 Spring Boot 出现之前,SpringSecurity 的使用场景是被另外一个安全管理框架 Shiro 牢牢霸占的,因为相对于 SpringSecurity 来说,SSM 中整合 Shiro 更加轻量级。Spring Boot 出现后,使这一情况情况大有改观。正应了那句古话:一人得道鸡犬升天,虽然有点不大合适,就将就着用吧。

这是因为 Spring Boot 为 SpringSecurity 提供了自动化配置,大大降低了 SpringSecurity 的学习成本。另外,SpringSecurity 的功能也比 Shiro 更加强大。

二、关于 JWT

JWT,是目前最流行的一个跨域认证解决方案:客户端发起用户登录请求,服务器端接收并认证成功后,生成一个 JSON 对象(如下所示),然后将其返回给客户端。

从本质上来说,JWT 就像是一种生成加密用户身份信息的 Token,更安全也更灵活。

三、整合步骤

第一步,给需要登录认证的模块添加 codingmore-security 依赖:

<dependency>
    <groupId>top.codingmore</groupId>
    <artifactId>codingmore-security</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

比如说 codingmore-admin 后端管理模块需要登录认证,就在 codingmore-admin/pom.xml 文件中添加 codingmore-security 依赖。

第二步,在需要登录认证的模块里添加 CodingmoreSecurityConfig 类,继承自 codingmore-security 模块中的 SecurityConfig 类。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CodingmoreSecurityConfig extends SecurityConfig {
    @Autowired
    private IUsersService usersService;

    @Bean
    public UserDetailsService userDetailsService() {
        //获取登录用户信息
        return username -> usersService.loadUserByUsername(username);
    }
}

UserDetailsService 这个类主要是用来加载用户信息的,包括用户名、密码、权限、角色集合....其中有一个方法如下:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

认证逻辑中,SpringSecurity 会调用这个方法根据客户端传入的用户名加载该用户的详细信息,包括判断:

  • 密码是否一致
  • 通过后获取权限和角色
    public UserDetails loadUserByUsername(String username) {
        // 根据用户名查询用户
        Users admin = getAdminByUsername(username);
        if (admin != null) {
            List<Resource> resourceList = getResourceList(admin.getId());
            return new AdminUserDetails(admin,resourceList);
        }
        throw new UsernameNotFoundException("用户名或密码错误");
    }

getAdminByUsername 负责根据用户名从数据库中查询出密码、角色、权限等。

    public Users getAdminByUsername(String username) {
        QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_login", username);
        List<Users> usersList = baseMapper.selectList(queryWrapper);

        if (usersList != null && usersList.size() > 0) {
            return usersList.get(0);
        }

        // 用户名错误,提前抛出异常
        throw new UsernameNotFoundException("用户名错误");
    }

第三步,在 application.yml 中配置下不需要安全保护的资源路径:

secure:
  ignored:
    urls: #安全路径白名单
      - /doc.html
      - /swagger-ui/**
      - /swagger/**
      - /swagger-resources/**
      - /**/v3/api-docs
      - /**/*.js
      - /**/*.css
      - /**/*.png
      - /**/*.ico
      - /webjars/springfox-swagger-ui/**
      - /actuator/**
      - /druid/**
      - /users/login
      - /users/register
      - /users/info
      - /users/logout

第四步,在登录接口中添加登录和刷新 token 的方法:

@Controller
@Api(tags = "用户")
@RequestMapping("/users")
public class UsersController {
    @Autowired
    private IUsersService usersService;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

@ApiOperation(value = "登录以后返回token")
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
        String token = usersService.login(users.getUserLogin(), users.getUserPass());

        if (token == null) {
            return ResultObject.validateFailed("用户名或密码错误");
        }

        // 将 JWT 传递回客户端
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        return ResultObject.success(tokenMap);
    }

    @ApiOperation(value = "刷新token")
    @RequestMapping(value = "/refreshToken", method = RequestMethod.GET)
    @ResponseBody
    public ResultObject refreshToken(HttpServletRequest request) {
        String token = request.getHeader(tokenHeader);
        String refreshToken = usersService.refreshToken(token);
        if (refreshToken == null) {
            return ResultObject.failed("token已经过期!");
        }
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", refreshToken);
        tokenMap.put("tokenHead", tokenHead);
        return ResultObject.success(tokenMap);
    }
}

使用 Apipost 来测试一下,首先是文章获取接口,在没有登录的情况下会提示暂未登录或者 token 已过期。

四、实现原理

小二之所以能仅用四步就实现了登录认证,主要是因为他将 SpringSecurity+JWT 的代码封装成了通用模块,我们来看看 codingmore-security 的目录结构。

codingmore-security
├── component
|    ├── JwtAuthenticationTokenFilter -- JWT登录授权过滤器
|    ├── RestAuthenticationEntryPoint
|    └── RestfulAccessDeniedHandler
├── config
|    ├── IgnoreUrlsConfig
|    └── SecurityConfig
└── util
     └── JwtTokenUtil -- JWT的token处理工具类

JwtAuthenticationTokenFilter 和 JwtTokenUtil 在讲 JWT 的时候已经详细地讲过了,这里再简单补充一点。

客户端的请求头里携带了 token,服务端肯定是需要针对每次请求解析校验 token 的,所以必须得定义一个过滤器,也就是 JwtAuthenticationTokenFilter:

  • 从请求头中获取 token
  • 对 token 进行解析、验签、校验过期时间
  • 校验成功,将验证结果放到 ThreadLocal 中,供下次请求使用

重点来看其他四个类。第一个 RestAuthenticationEntryPoint(自定义返回结果:未登录或登录过期):

public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Cache-Control","no-cache");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(ResultObject.unauthorized(authException.getMessage())));
        response.getWriter().flush();
    }
}

可以通过 debug 的方式看一下返回的信息正是之前用户未登录状态下访问文章页的错误信息。

具体的信息是在 ResultCode 类中定义的。

public enum ResultCode implements IErrorCode {
    SUCCESS(0, "操作成功"),
    FAILED(500, "操作失败"),
    VALIDATE_FAILED(506, "参数检验失败"),
    UNAUTHORIZED(401, "暂未登录或token已经过期"),
    FORBIDDEN(403, "没有相关权限");
    private long code;
    private String message;

    private ResultCode(long code, String message) {
        this.code = code;
        this.message = message;
    }
}

第二个 RestfulAccessDeniedHandler(自定义返回结果:没有权限访问时):

public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Cache-Control","no-cache");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(ResultObject.forbidden(e.getMessage())));
        response.getWriter().flush();
    }
}

第三个IgnoreUrlsConfig(用于配置不需要安全保护的资源路径):

@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
    private List<String> urls = new ArrayList<>();
}

通过 lombok 注解的方式直接将配置文件中不需要权限校验的路径放开,比如说 Knife4j 的接口文档页面。如果不放开的话,就被 SpringSecurity 拦截了,没办法访问到了。

第四个SecurityConfig(SpringSecurity通用配置):

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired(required = false)
    private DynamicSecurityService dynamicSecurityService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
                .authorizeRequests();

        //不需要保护的资源路径允许访问
        for (String url : ignoreUrlsConfig().getUrls()) {
            registry.antMatchers(url).permitAll();
        }

        // 任何请求需要身份认证
        registry.and()
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                // 关闭跨站请求防护及不使用session
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 自定义权限拒绝处理类
                .and()
                .exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler())
                .authenticationEntryPoint(restAuthenticationEntryPoint())
                // 自定义权限拦截器JWT过滤器
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //有动态权限配置时添加动态权限校验过滤器
        if(dynamicSecurityService!=null){
            registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
        }
    }
}

这个类的主要作用就是告诉 SpringSecurity 那些路径不需要拦截,除此之外的,都要进行 RestfulAccessDeniedHandler(登录校验)、RestAuthenticationEntryPoint(权限校验)和 JwtAuthenticationTokenFilter(JWT 过滤)。

并且将 JwtAuthenticationTokenFilter 过滤器添加到 UsernamePasswordAuthenticationFilter 过滤器之前。

五、测试

第一步,测试登录接口,Apipost 直接访问 http://localhost:9002/users/login,可以看到 token 正常返回。

第二步,不带 token 直接访问文章接口,可以看到进入了 RestAuthenticationEntryPoint 这个处理器:

第三步,携带 token,这次我们改用 Knife4j 来测试,发现可以正常访问:

源码链接:

https://github.com/itwanger/coding-more

本篇已收录至 GitHub 上星标 1.9k+ star 的开源专栏《Java 程序员进阶之路》,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。学 Java,就认准 Java 程序员进阶之路?。

https://github.com/itwanger/toBeBetterJavaer

star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。也可以戳下面的链接跳转到《Java 程序员进阶之路》的官网网址,开始愉快的学习之旅吧。

https://tobebetterjavaer.com/

没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟

有关厉害!我带的实习生仅用四步就整合好SpringSecurity+JWT实现登录认证!的更多相关文章

  1. ruby - token 认证 - 2

    简单代码require'net/http'url=URI.parse('getjson/otherdatahere[link]')req=Net::HTTP::Get.new(url.to_s)res=Net::HTTP.start(url.host,url.port){|http|http.request(req)}putsres.body只是想知道如何在phpcURL中放置身份验证token,我是这样做的    curl_setopt($ch,CURLOPT_HTTPHEADER,array('Authorization:Bearerxxx'));//Bearertokenfora

  2. ruby - HTTParty 摘要认证 - 2

    谁能提供一个使用HTTParty和digestauth的例子?我在网上找不到例子,希望有人能提供一些帮助。谢谢。 最佳答案 您可以在定义类时使用digest_auth方法设置用户名和密码classFooincludeHTTPartydigest_auth'username','password'end 关于ruby-HTTParty摘要认证,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questi

  3. 华为认证的网络工程师证好考吗,含金量高吗 ? - 2

    华为认证分等级的,相当于初中高三个等级,当然高级是比较难考的,也是含金量最高的。我就慢慢给你介绍一下。1.了解华为认证华为认证网络工程师是由华为公司认证与采购部推出的独立认证体系,与之前的华为认证不同,简称HCIA。同时华为认证是华为技术有限公司凭借多年信息通信技术人才培养经验,以及对行业发展的理解,以层次化的职业技术认证为指引,推出的覆盖IP、IT、CT以及ICT融合技术领域的认证体系,是ICT全技术领域认证体系。​2.怎么考取华为认证网络工程师?要考取华为认证网络工程师必须选择最近的Prometric授权考试中心APTC报名并参加GB0-190的考试,考试通过后,以获得由华为统一签发的“华

  4. 2023年6月DAMA-CDGP数据治理专家认证请尽快报名啦! - 2

    目前6月DAMA-CDGP数据治理认证考试开放报名地区有:北京、上海、广州、深圳、长沙、呼和浩特。目前南京、济南、西安、杭州等地区还在接近开考人数中,打算参加6月考试的朋友们可以抓紧时间报名啦!!!5月初,DAMA-CDGA/CDGP数据治理认证考前班也即将开班啦!报名从速!!!DAMA认证为数据管理专业人士提供职业目标晋升规划,彰显了职业发展里程碑及发展阶梯定义,帮助数据管理从业人士获得企业数字化转型战略下的必备职业能力,促进开展工作实践应用及实际问题解决,形成企业所需的新数字经济下的核心职业竞争能力。DAMA是数据管理方面的认证,帮助数据从业者提升数据管理能力。CDGP数据治理专家认证属于

  5. ruby-on-rails - 在 mongoid.yml 中指定认证数据库 - 2

    我正在尝试通过mongoid.yml进行身份验证,但我想要进行身份验证的用户在admin数据库中。如果我尝试在数据库字段中指定管理数据库,它也会将所有集合放在那里,我不希望这样。有没有办法将我的数据库字段设置为我想要的数据库,但指定一个单独的身份验证数据库?这是我的mongoid.yml文件development:sessions:default:database:XC_DEVhosts:-IP:PORTusername:userpassword:password 最佳答案 这个问题已经存在一年多了,但值得回答。是的,auth_so

  6. 华为鲲鹏认证怎么样?值得考吗 - 2

    华为鲲鹏认证是华为新推出的认证,可以看看下它的介绍:HCIA-KunpengApplicationDeveloper认证定位于培养和认证能够应用华为鲲鹏计算平台进行应用迁移与部署,性能测试与调优的鲲鹏应用开发工程师。获得HCIA-KunpengApplicationDeveloper认证,证明学员对华为鲲鹏计算平台有初步的了解,能够基于华为鲲鹏计算平台进行应用迁移、部署、性能测试和调优等工作。HCIP-KunpengApplicationDeveloper认证定位于培训与认证能够基于鲲鹏架构进行应用开发和高阶鲲鹏应用移植的高级工程师,通过此认证,将证明您了解鲲鹏应用从底层到发布的整个生命周期各

  7. Spring Security详细讲解(JWT+SpringSecurity登入案例) - 2

    本篇博文目录:一.SpringSecurity简介1.SpringSecurity2.SpringSecurity相关概念二.认证和授权1.认证(1)使用SpringSecurity进行简单的认证(SpringBoot项目中)(2)SpringSecurity的原理(3)SpringSecurity核心类(4)认证登入案例(JWT+SpringSecurity实现登入案例)2.授权(1)加入权限到Authentication中(2)SecurityConfig配置文件中开启注解权限配置(3)给接口中的方法添加访问权限(4)用户权限表的建立3.自定义失败处理(1)创建异常处理类(2)配置移除处理

  8. SpringSecurity 源码理解及使用(三) - 2

    目录springSecurity授权权限管理策略基于url的权限管理基于方法的权限管理将url权限管理设为动态会话管理会话并发管理会话失效处理禁止再次登录会话共享源码分析CSRF跨站请求伪造开启CSRF防御传统web开发前后端分离开启CSRF防护csrf防御过程CORS跨域问题springBoot解决跨域的三种方式springSecurity解决跨域springSecurity授权认证与授权解耦授权:据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有权限,去执行相应操作。GrantedAuthority应该如何理解呢?是角色还是权限?权限是具体一些操作,角色是一些权

  9. javascript - JWT解码返回null - 2

    我是Node.js的新手,正在阅读FabianCook的Node.jsEssentials。当尝试使用JWT进行身份验证时,我从jwt.decode(token)得到了一个NULL,但该token可以由jwt.io上的调试器解析。代码有什么问题?varPassport=require('passport');varLocalStrategy=require('passport-local').Strategy;varExpress=require('express');varBodyParser=require('body-parser');varjwt=require('jsonwe

  10. javascript - 如何将 jwt 身份验证与 ACL 混合使用 - 2

    我已经能够使用JWT策略实现Passport,并且效果很好。我的受jwt保护的路由看起来像这样......app.get('/thingThatRequiresLogin/:id',passport.authenticate('jwt',{session:false}),thingThatRequiresLogin.findById);现在我需要将某些内容的访问权限限制为仅属于某个Angular色的已登录用户。我希望我能这样表达:app.get('/thingThatRequiresLogin/:id',MAGIC,thingThatRequiresLogin.findById);MA

随机推荐