当前位置: 首页 > 科技观察

锋利的!我带的实习生集成SpringSecurity+JWT只需要四步就可以实现登录认证!

时间:2023-03-19 17:43:06 科技观察

小二是新来的实习生。作为技术负责人,我还是很有责任感的。我想把一切都归咎于他。啊,不对,我怎么会不小心把心里话说出来呢?重磅加油!小二是新来的实习生。作为技术负责人,我还是很有责任感的。当我有什么好事的时候,我总是想起他。这不,我安排了一个集成SpringSecurity+JWT实现登录认证的任务。没想到,他只用了四步就搞定了,让我忍不住当场称赞:太强了,兄弟!以下是他完成任务时做的笔记。看完之后只有一个感受:文笔虽然年轻但是口才好,简直就是公司未来的栋梁之才,各大技术社区的博主之星。嗯嗯嗯嗯嗯,分享出来,给大家一个表扬的机会(吐槽):请大家在评论区火力全开,不要顾及我的面子。1、关于SpringSecurity在SpringBoot出现之前,SpringSecurity的使用场景被另一个安全管理框架Shiro牢牢占据,因为相对于SpringSecurity,Shiro在SSM中的集成更为轻量级。SpringBoot出现后,这种情况有了很大的改善。正应了那句古话:一人成道虽不妥,终必上天。这是因为SpringBoot为SpringSecurity提供了自动配置,大大降低了SpringSecurity的学习成本。另外,SpringSecurity比Shiro更强大。2、关于JWTJWT是目前最流行的跨域认证方案:客户端发起用户登录请求,服务端接收并认证成功后,生成一个JSON对象(如下图),然后返回给客户。从本质上讲,JWT就像是生成加密用户身份信息的Token,更加安全灵活。3.集成步骤第一步是为需要登录认证的模块添加codingmore-security依赖:top.codingmorecodingmore-security1.0-SNAPSHOT比如codingmore-admin后台管理模块需要登录认证,在codingmore-admin/pom.xml文件中添加codingmore-security依赖。第二步,在需要登录认证的模块中添加CodingmoreSecurityConfig类,该类继承自codingmore-security模块中的SecurityConfig类。@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled=true)publicclassCodingmoreSecurityConfigextendsSecurityConfig{@AutowiredprivateIUsersServiceusersService;@BeanpublicUserDetailsS??erviceuserDetailsS??ervice(){//获取登录用户信息returnusername->usersname);User}.loadUserDetailsS??ervice该类主要用于加载用户信息,包括用户名、密码、权限、角色集合。其中一种方法如下:UserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException;在认证逻辑中,SpringSecurity会调用该方法根据客户端传入的用户名加载用户的详细信息,包括判断密码是否一致,传递获取权限和角色publicUserDetailsloadUserByUsername(Stringusername){//查询基于用户名的用户Usersadmin=getAdminByUsername(username);if(admin!=null){ListresourceList=getResourceList(admin.getId());返回新的AdminUserDetails(admin,resourceList);}thrownewUsernameNotFoundException("用户名或密码错误");}getAdminByUsername负责root根据用户名从数据库中查询密码、角色、权限等publicUsersgetAdminByUsername(Stringusername){QueryWrapperqueryWrapper=newQueryWrapper<>();queryWrapper.eq("user_login",用户名);ListusersList=baseMapper.selectList(queryWrapper);if(usersList!=null&&usersList.size()>0){returnusersList.get(0);}//用户名错误,提前抛出异常thrownewUsernameNotFoundException("用户名错误");第三步在application.yml中配置不需要安全保护的资源路径:secure:ignored:urls:#Securitypathwhitelist-/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第四步添加登录界面中的登录和刷新令牌方法:@Controller@Api(tags="Users")@RequestMapping("/users")publicclassUsersController{@AutowiredprivateIUsersServiceusersService;@Value("${jwt.tokenHeader}")privateStringtokenHeader;@Value("${jwt.tokenHead}")privateStringtokenHead;@ApiOperation(value="登录以后返回token")@RequestMapping(value="/login",method=RequestMethod.POST)@ResponseBodypublicResultObjectlogin(@ValidatedUsersLoginParam用户,BindingResult结果){Stringtoken=usersService.login(users.getUserLogin(),users.getUserPass());if(token==null){returnResultObject.validateFailed("用户名或密码错误");}//将JWT传回客户端MaptokenMap=newHashMap<>();tokenMap.put("令牌",令牌);tokenMap.put("tokenHead",tokenHead);返回ResultObject.success(tokenMap);}@ApiOperation(value="刷新令牌")@RequestMapping(value="/refreshToken",method=RequestMethod.GET)@ResponseBodypublicResultObjectrefreshToken(HttpServletRequest请求){Stringtoken=request.getHeader(tokenHeader);StringrefreshToken=usersService.refreshToken(token);if(refreshToken==null){returnResultObject.failed("tokenhasexpired!");}MaptokenMap=newHashMap<>();tokenMap.put("令牌",refreshToken);tokenMap.put("tokenHead",tokenHead);返回ResultObject.success(tokenMap);}}使用Apipost进行测试,首先是文章获取接口。如果没有登录,会提示你还没有登录或者token已经过期。4.实现原理小二之所以只用四步就可以实现登录认证,主要是因为他把SpringSecurity+JWT的代码封装成了一个通用模块,我们来看看codingmore-security的目录结构。codingmore-security├──组件|├──JwtAuthenticationTokenFilter--JWT登录授权过滤器|├──RestAuthenticationEntryPoint|└──RestfulAccessDeniedHandler├──配置|├──IgnoreUrlsConfig|└──SecurityConfig└──util└──JwtTokenUtil--JWT的token处理工具类JwtAuthenticationTokenFilter和JwtTokenUtil在讲JWT的时候已经详细讲过了,这里简单补充一下。客户端的请求头携带token,服务端每次请求都要解析和验证token,所以必须定义一个过滤器,即JwtAuthenticationTokenFilter:从请求头中获取token进行解析,验证token,Verifythatthe过期时间验证成功,将验证结果放入ThreadLocal,供下次请求使用。关注其他四个类。第一个RestAuthenticationEntryPoint(自定义返回结果:未登录或登录过期):publicclassRestAuthenticationEntryPointimplementsAuthenticationEntryPoint{@Overridepublicvoidcommine(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionauthException)throwsIOException,ServletExceptionresponse.setHeader("Access-Control-Allow-Origin","*");response.setHeader("缓存控制","无缓存");response.setCharacterEncoding("UTF-8");response.setContentType("application/json");response.getWriter().println(JSONUtil.parse(ResultObject.unauthorized(authException.getMessage())));response.getWriter().flush();}}可以通过调试用户未登录访问文章页面时的Errormessage查看返回信息。具体信息定义在ResultCode类中。publicenumResultCodeimplementsIErrorCode{SUCCESS(0,"操作成功"),FAILED(500,"操作失败"),VALIDATE_FAILED(506,"参数验证失败"),UNAUTHORIZED(401,"尚未登录或令牌已过期"),FORBIDDEN(403,"无相关权限");私有长码;私人字符串消息;privateResultCode(longcode,Stringmessage){this.code=code;this.message=消息;}}第二个RestfulAccessDeniedHandler(自定义返回结果:未授权访问时):publicclassRestfulAccessDeniedHandlerimplementsAccessDeniedHandler{@Overridepublicvoidhandle(HttpServletRequestrequest,HttpServletResponseresponse,AccessDeniedExceptione)throwsIOException,ServletException-Access{response.setHead,"*");response.setHeader("缓存控制","无缓存");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")publicclassIgnoreUrlsConfig{privateListurls=newArrayList<>();}通过lombok注解直接释放配置文件中不需要进行权限验证的路径,比如Knife4j的接口文档页面如果不释放,会被SpringSecurity拦截,无法访问.第四个SecurityConfig(SpringSecurity通用配置):publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@Autowired(required=false)privateDynamicSecurityServicedynamicSecurityService;@Overrideprotectedvoidconfigure(HttpSecurityhttpSecurity)抛出异常//允许不需要保护的资源路径访问for(Stringurl:ignoreUrlsConfig().getUrls()){registry.antMatchers(url).permitAll();}//任何请求都需要身份验证registry.and().authorizeRequests().anyRequest().authenticated()//关闭跨站请求保护,不使用会话.and().csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//来自DefinepermissiondenialsHandlingclass.and().exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler()).authenticationEntryPoint(restAuthenticationEntryPoint())//自定义权限拦截器JWTfilter.and().addFilterBefore(jwtAuthenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);//有动态权限配置时添加动态权限验证过滤器}}}这个类的主要作用就是告诉SpringSecurity那些路径不需要拦截。否则,必须执行RestfulAccessDeniedHandler(登录验证)、RestAuthenticationEntryPoint(权限验证)和JwtAuthenticationTokenFilter(JWT过滤),并在UsernamePasswordAuthenticationFilter过滤器之前添加JwtAuthenticationTokenFilter过滤器。5.测试第一步是测试登录界面。apipost直接访问http://localhost:9002/users/login,可以看到token正常返回。第二步,无需token,直接访问文章界面。可以看到RestAuthenticationEntryPoint处理器进入了:第三步,携带token。这次我们使用Knife4j进行测试,发现可以正常访问:源码链接:https://github.com/itwanger/coding-more参考链接:http://www.macrozheng.com/https://juejin.cn/post/7036556688303849480