SpringBoot集成SpringSecurity和JWT做登陆鉴权的实现
废话
目前流行的前后端分离让Java程序员可以更加专注的做好后台业务逻辑的功能实现,提供如返回Json格式的数据接口就可以。SpringBoot的易用性和对其他框架的高度集成,用来快速开发一个小型应用是最佳的选择。
一套前后端分离的后台项目,刚开始就要面对的就是登陆和授权的问题。这里提供一套方案供大家参考。
主要看点:
- 登陆后获取token,根据token来请求资源
- 根据用户角色来确定对资源的访问权限
- 统一异常处理
- 返回标准的Json格式数据
正文
首先是pom文件:
org.springframework.boot spring-boot-starter org.projectlombok lombok true org.springframework.boot spring-boot-starter-data-solr org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter 1.3.2 mysql mysql-connector-java runtime org.springframework.boot spring-boot-configuration-processor true io.springfox springfox-swagger2 2.6.1 io.springfox springfox-swagger-ui 2.6.1 org.springframework.boot spring-boot-starter-data-rest org.springframework.boot spring-boot-starter-security org.springframework.security spring-security-jwt 1.0.9.RELEASE io.jsonwebtoken jjwt 0.9.0 org.springframework.boot spring-boot-starter-test test
application.yml:
spring: datasource: url:jdbc:mysql://127.0.0.1:3306/les_data_center?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useAffectedRows=true&useSSL=false username:root password:123456 driverClassName:com.mysql.jdbc.Driver jackson: data-format:yyyy-MM-ddHH:mm:ss time-zone:GMT+8 mybatis: config-location:classpath:/mybatis-config.xml #JWT jwt: header:Authorization secret:mySecret #token有效期一天 expiration:86400 tokenHead:"Bearer"
接着是对security的配置,让security来保护我们的API
SpringBoot推荐使用配置类来代替xml配置。那这里,我也使用配置类的方式。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled=true) publicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{ privatefinalJwtAuthenticationEntryPointunauthorizedHandler; privatefinalAccessDeniedHandleraccessDeniedHandler; privatefinalUserDetailsServiceCustomUserDetailsService; privatefinalJwtAuthenticationTokenFilterauthenticationTokenFilter; @Autowired publicWebSecurityConfig(JwtAuthenticationEntryPointunauthorizedHandler, @Qualifier("RestAuthenticationAccessDeniedHandler")AccessDeniedHandleraccessDeniedHandler, @Qualifier("CustomUserDetailsService")UserDetailsServiceCustomUserDetailsService, JwtAuthenticationTokenFilterauthenticationTokenFilter){ this.unauthorizedHandler=unauthorizedHandler; this.accessDeniedHandler=accessDeniedHandler; this.CustomUserDetailsService=CustomUserDetailsService; this.authenticationTokenFilter=authenticationTokenFilter; } @Autowired publicvoidconfigureAuthentication(AuthenticationManagerBuilderauthenticationManagerBuilder)throwsException{ authenticationManagerBuilder //设置UserDetailsService .userDetailsService(this.CustomUserDetailsService) //使用BCrypt进行密码的hash .passwordEncoder(passwordEncoder()); } //装载BCrypt密码编码器 @Bean publicPasswordEncoderpasswordEncoder(){ returnnewBCryptPasswordEncoder(); } @Override protectedvoidconfigure(HttpSecurityhttpSecurity)throwsException{ httpSecurity .exceptionHandling().accessDeniedHandler(accessDeniedHandler).and() //由于使用的是JWT,我们这里不需要csrf .csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() //基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() //对于获取token的restapi要允许匿名访问 .antMatchers("/api/v1/auth","/api/v1/signout","/error/**","/api/**").permitAll() //除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //禁用缓存 httpSecurity.headers().cacheControl(); //添加JWTfilter httpSecurity .addFilterBefore(authenticationTokenFilter,UsernamePasswordAuthenticationFilter.class); } @Override publicvoidconfigure(WebSecurityweb)throwsException{ web.ignoring().antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui", "/swagger-resources", "/swagger-resources/configuration/security", "/swagger-ui.html" ); } @Bean @Override publicAuthenticationManagerauthenticationManagerBean()throwsException{ returnsuper.authenticationManagerBean(); } }
该类中配置了几个bean来供security使用。
- JwtAuthenticationTokenFilter:token过滤器来验证token有效性
- UserDetailsService:实现了DetailsService接口,用来做登陆验证
- JwtAuthenticationEntryPoint:认证失败处理类
- RestAuthenticationAccessDeniedHandler:权限不足处理类
那么,接下来一个一个实现这些类:
/** *token校验,引用的stackoverflow一个答案里的处理方式 *Author:JoeTao *createAt:2018/9/14 */ @Component publicclassJwtAuthenticationTokenFilterextendsOncePerRequestFilter{ @Value("${jwt.header}") privateStringtoken_header; @Resource privateJWTUtilsjwtUtils; @Override protectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainchain)throwsServletException,IOException{ Stringauth_token=request.getHeader(this.token_header); finalStringauth_token_start="Bearer"; if(StringUtils.isNotEmpty(auth_token)&&auth_token.startsWith(auth_token_start)){ auth_token=auth_token.substring(auth_token_start.length()); }else{ //不按规范,不允许通过验证 auth_token=null; } Stringusername=jwtUtils.getUsernameFromToken(auth_token); logger.info(String.format("Checkingauthenticationforuser%s.",username)); if(username!=null&&SecurityContextHolder.getContext().getAuthentication()==null){ Useruser=jwtUtils.getUserFromToken(auth_token); if(jwtUtils.validateToken(auth_token,user)){ UsernamePasswordAuthenticationTokenauthentication=newUsernamePasswordAuthenticationToken(user,null,user.getAuthorities()); authentication.setDetails(newWebAuthenticationDetailsSource().buildDetails(request)); logger.info(String.format("Authenticateduser%s,settingsecuritycontext",username)); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(request,response); } }
/** *认证失败处理类,返回401 *Author:JoeTao *createAt:2018/9/20 */ @Component publicclassJwtAuthenticationEntryPointimplementsAuthenticationEntryPoint,Serializable{ privatestaticfinallongserialVersionUID=-8970718410437077606L; @Override publicvoidcommence(HttpServletRequestrequest, HttpServletResponseresponse, AuthenticationExceptionauthException)throwsIOException{ //验证为未登陆状态会进入此方法,认证错误 System.out.println("认证失败:"+authException.getMessage()); response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=utf-8"); PrintWriterprintWriter=response.getWriter(); Stringbody=ResultJson.failure(ResultCode.UNAUTHORIZED,authException.getMessage()).toString(); printWriter.write(body); printWriter.flush(); } }
因为我们使用的RESTAPI,所以我们认为到达后台的请求都是正常的,所以返回的HTTP状态码都是200,用接口返回的code来确定请求是否正常。
/** *权限不足处理类,返回403 *Author:JoeTao *createAt:2018/9/21 */ @Component("RestAuthenticationAccessDeniedHandler") publicclassRestAuthenticationAccessDeniedHandlerimplementsAccessDeniedHandler{ @Override publicvoidhandle(HttpServletRequesthttpServletRequest,HttpServletResponseresponse,AccessDeniedExceptione)throwsIOException,ServletException{ //登陆状态下,权限不足执行该方法 System.out.println("权限不足:"+e.getMessage()); response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=utf-8"); PrintWriterprintWriter=response.getWriter(); Stringbody=ResultJson.failure(ResultCode.FORBIDDEN,e.getMessage()).toString(); printWriter.write(body); printWriter.flush(); } }
/** *登陆身份认证 *Author:JoeTao *createAt:2018/9/14 */ @Component(value="CustomUserDetailsService") publicclassCustomUserDetailsServiceimplementsUserDetailsService{ privatefinalAuthMapperauthMapper; publicCustomUserDetailsService(AuthMapperauthMapper){ this.authMapper=authMapper; } @Override publicUserloadUserByUsername(Stringname)throwsUsernameNotFoundException{ Useruser=authMapper.findByUsername(name); if(user==null){ thrownewUsernameNotFoundException(String.format("Nouserfoundwithusername'%s'.",name)); } Rolerole=authMapper.findRoleByUserId(user.getId()); user.setRole(role); returnuser; } }
登陆逻辑:
publicResponseUserTokenlogin(Stringusername,Stringpassword){ //用户验证 finalAuthenticationauthentication=authenticate(username,password); //存储认证信息 SecurityContextHolder.getContext().setAuthentication(authentication); //生成token finalUseruser=(User)authentication.getPrincipal(); //Useruser=(User)userDetailsService.loadUserByUsername(username); finalStringtoken=jwtTokenUtil.generateAccessToken(user); //存储token jwtTokenUtil.putToken(username,token); returnnewResponseUserToken(token,user); } privateAuthenticationauthenticate(Stringusername,Stringpassword){ try{ //该方法会去调用userDetailsService.loadUserByUsername()去验证用户名和密码,如果正确,则存储该用户名密码到“security的context中” returnauthenticationManager.authenticate(newUsernamePasswordAuthenticationToken(username,password)); }catch(DisabledException|BadCredentialsExceptione){ thrownewCustomException(ResultJson.failure(ResultCode.LOGIN_ERROR,e.getMessage())); } }
自定义异常:
@Getter publicclassCustomExceptionextendsRuntimeException{ privateResultJsonresultJson; publicCustomException(ResultJsonresultJson){ this.resultJson=resultJson; } }
统一异常处理:
/** *异常处理类 *controller层异常无法捕获处理,需要自己处理 *Createdbyjton2018/8/27. */ @RestControllerAdvice @Slf4j publicclassDefaultExceptionHandler{ /** *处理所有自定义异常 *@parame *@return */ @ExceptionHandler(CustomException.class) publicResultJsonhandleCustomException(CustomExceptione){ log.error(e.getResultJson().getMsg().toString()); returne.getResultJson(); } }
所有经controller转发的请求抛出的自定义异常都会被捕获处理,一般情况下就是返回给调用方一个json的报错信息,包含自定义状态码、错误信息及补充描述信息。
值得注意的是,在请求到达controller之前,会被Filter拦截,如果在controller或者之前抛出的异常,自定义的异常处理器是无法处理的,需要自己重新定义一个全局异常处理器或者直接处理。
Filter拦截请求两次的问题
跨域的post的请求会验证两次,get不会。网上的解释是,post请求第一次是预检请求,RequestMethod:OPTIONS。
解决方法:
在webSecurityConfig里添加
.antMatchers(HttpMethod.OPTIONS,"/**").permitAll()
就可以不拦截options请求了。
这里只给出了最主要的代码,还有controller层的访问权限设置,返回状态码,返回类定义等等。
所有代码已上传GitHub,项目地址
到此这篇关于SpringBoot集成SpringSecurity和JWT做登陆鉴权的实现的文章就介绍到这了,更多相关SpringBootJWT登陆鉴权内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。