写在前面的话
小白记录第一次整合Springboot+Springsecurity+hutool+redis+jwt。
本文章使用了大量hutool的工具类,请悉知。
本文章中心是携带验证码+账号密码请求后端验证,使用Redis存储验证码。
默认在阅读本篇文章的朋友们对Springboot、SpringSecurity、redis、jwt已有认知。
因为本项目属于本人的一个练手项目,所以包含的一些pom依赖如不需要请自行剥离,仅需SpringSecurity、redis、jwt即可。
如有疑问请评论区友好交流指点。
引入相关依赖
<properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.3.7.RELEASE</spring-boot.version><druid.version>1.2.4</druid.version><hutool.version>5.7.16</hutool.version><poi.version>5.1.0</poi.version><mybatis-plus.version>3.4.1</mybatis-plus.version><mybatis-plus-velocity.version>2.2</mybatis-plus-velocity.version><jwt.version>0.11.2</jwt.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><!--springbootweb--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--springboot2.3.1级更高,springboot不再内置验证--><!--<dependency><groupId>jakarta.validation</groupId><artifactId>jakarta.validation-api</artifactId><version>${jakarta.version}</version></dependency>--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!--springaop面向切面自定义注解需要--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!--druid--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid.version}</version></dependency><!--hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>${hutool.version}</version></dependency><!--poi--><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>${poi.version}</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>${poi.version}</version></dependency><!--mybatis-plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><!--mybatis-plus代码生成器--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>${mybatis-plus.version}</version></dependency><!--mybatis-plus模板引擎默认--><dependency><groupId>org.apache.velocity</groupId><artifactId>velocity-engine-core</artifactId><version>${mybatis-plus-velocity.version}</version></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--spring-security--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--jwt所需jar包--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>${jwt.version}</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>${jwt.version}</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>${jwt.version}</version><scope>runtime</scope></dependency></dependencies>
验证码存在redis中,并返回base64到前端
本文章使用的是redis存储验证码,具体组成为key:code_xxx,value:英文数字(四位)
验证码图返回的base64使用的是hutool工具类(强烈推荐)
controller
//randomCode是一个时间戳,由前端生成后请求后端,具体是防止redis中的key重复@ApiOperation(value="验证码",notes="获取验证码")@GetMapping("/getRandomCode")publicResultgetRandomCode(@RequestParamStringrandomCode){if(ObjectUtil.isEmpty(randomCode)){returnResult.error(500,"请输入验证码!");}returnResult.successData(loginService.getRandomCode(randomCode));}
service
/***获取验证码Base64**@paramrandomCode*@return*/@Transactional(propagation=Propagation.SUPPORTS,rollbackFor=Exception.class)@OverridepublicStringgetRandomCode(StringrandomCode){//定义图形验证码的长、宽、验证码字符数、干扰元素个数ShearCaptchacaptcha=CaptchaUtil.createShearCaptcha(90,34,4,3);//设置背景颜色captcha.setBackground(Color.WHITE);//验证图形验证码的有效性,返回boolean值captcha.verify("60");//将字符长存入redis,并判断redis中是否存在//RedisUtil,我一会贴在下面//TimeUnit是个枚举类,我这里选择是以秒计时,如60秒后过期清除当前验证码booleanredisCode=RedisUtil.set("code_"+randomCode,captcha.getCode(),过期时长,TimeUnit.SECONDS);//如果存入redis中失败,抛出异常//这里是自定义异常类,可以自行处理,不影响if(!redisCode){newBusinessException(状态码,返回提示信息);}//3.这里只返回Base64字符串用来展示returncaptcha.getImageBase64Data();}
RedisUtil工具类
/***掘金里无法导入整个redis工具类,我这里挑了几个需要的方法,仅供参考*redis工具类*@authoryueranzs*@date2021-03-0410:08*/publicclassRedisUtil{//因为普通类无法直接使用RedisTemplate,这里用hutool中的SpringUtil来获取bean//如没引入hutool的可以百度下springboot中java普通类怎么调用mapper或service中的接口//关键注解@Component、@PostConstructprivatestaticfinalRedisTemplateredisTemplate=SpringUtil.getBean("redisTemplate");/***普通缓存放入**@paramkey键*@paramvalue值*@returntrue成功false失败*/publicstaticbooleanset(Stringkey,Objectvalue){try{redisTemplate.opsForValue().set(key,value);returntrue;}catch(Exceptione){e.printStackTrace();returnfalse;}}/***删除缓存**@paramkey可以传一个值或多个*/@SuppressWarnings("unchecked")publicstaticvoiddel(String...key){if(key!=null&&key.length>0){if(key.length==1){redisTemplate.delete(key[0]);}else{redisTemplate.delete(CollectionUtils.arrayToList(key));}}}}
异常类
/***@authoryueranzs*@date2021-11-0317:55*/@Data@AllArgsConstructor@NoArgsConstructorpublicclassBusinessExceptionextendsRuntimeException{@ApiModelProperty(value="状态码")privateIntegercode;@ApiModelProperty(value="错误信息")privateStringerrMsg;}
全局异常处理
/***全局异常处理*@authoryueranzs*@date2021-11-0111:55*/@Slf4j@ControllerAdvicepublicclassGlobalExceptionHandler{/***这里的意思是,只要捕获到BusinessException异常,那么就执行此方法*/@ExceptionHandler(BusinessException.class)@ResponseBodypublicResulterror(BusinessExceptionexception){log.error(exception.getErrMsg());returnResult.error(exception.getCode(),exception.getErrMsg());}}
封装返回类
/***封装返回类*@authoryueranzs*@date2021-11-0110:51*/@DatapublicclassResult{@ApiModelProperty(value="是否成功")privateBooleansuccess;@ApiModelProperty(value="响应码")privateIntegercode;@ApiModelProperty(value="提示信息")privateStringmessage;@ApiModelProperty(value="返回数据")privateObjectdata;/***构造方法私有化,里面的方法都是静态方法*达到保护属性的作用*/privateResult(){}/***这里使用链式编程*@return*/publicstaticResultok(){Resultresult=newResult();result.setSuccess(true);result.setCode(ResultCode.SUCCESS.getCode());result.setMessage(ResultCode.SUCCESS.getMessage());returnresult;}publicstaticResultok(Integercode,Stringmessage){Resultresult=newResult();result.setSuccess(true);result.setCode(code);result.setMessage(message);returnresult;}publicstaticResulterror(){Resultresult=newResult();result.setSuccess(false);//失败coderesult.setCode(ResultCode.COMMON_FAIL.getCode());//失败messageresult.setMessage(ResultCode.COMMON_FAIL.getMessage());returnresult;}publicstaticResulterror(Integercode,Stringmessage){Resultresult=newResult();result.setSuccess(false);result.setCode(code);result.setMessage(message);returnresult;}publicstaticResultsuccessData(Objectdata){Resultresult=newResult();result.setSuccess(true);//成功coderesult.setCode(ResultCode.SUCCESS.getCode());//成功messageresult.setMessage(ResultCode.SUCCESS.getMessage());result.setData(data);returnresult;}publicstaticResulterrorData(Objectdata){Resultresult=newResult();result.setSuccess(false);//失败coderesult.setCode(ResultCode.COMMON_FAIL.getCode());//失败messageresult.setMessage(ResultCode.COMMON_FAIL.getMessage());result.setData(data);returnresult;}/***自定义*@paramsuccess*@return*/publicResultsuccess(Booleansuccess){this.setSuccess(success);returnthis;}publicResultmessage(Stringmessage){this.setMessage(message);returnthis;}publicResultcode(Integercode){this.setCode(code);returnthis;}publicResultdata(Objectdata){this.setData(data);returnthis;}}
访问获取验证码接口
postman请求返回的数据和结构
前端页面展示情况
redis中存储情况
编写SecurityConfig配置类
关于EnableGlobalMethodSecurity
当我们想要开启spring方法级安全时,只需要在任何 @Configuration 实例上使用 @EnableGlobalMethodSecurity 注解就能达到此目的。同时这个注解为我们提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三种不同的机制来实现同一种功能。
具体请访问链接,有详细解释:https://blog.csdn.net/chihaihai/article/details/104678864
/**
@author yueranzs
@date 2021/11/22 13:56 */ @Configuration //开启springsecurity @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private LoginAuthenticationProvider loginAuthenticationProvider; @Autowired private LoginUserDetails loginUserDetails; @Autowired private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
/**
/**
/**
/**
/**
@Override protected void configure(HttpSecurity http) throws Exception { //Springsecurity放行规则,permitAll是针对所有方法。 //目使用了swagger,所以需要将swagger相关的url放行。 //SpringseCurity的放行规则由上往下,如果前者已被拦截, //不再执行,所以这就是为什么.anyRequest().authenticated()需要放在最后的原因。 http.authorizeRequests() .antMatchers("/webjars/").permitAll() .antMatchers("/swagger-resources/").permitAll() .antMatchers("/v2/*").permitAll() .antMatchers("/login/**").permitAll() //剩下方法拦截 .anyRequest().authenticated() .and() .formLogin() //登录页 .loginPage("/login.html") //登录请求接口,如果url为空也会默认将loginPage的值赋值给url //可能习惯性会认为需要自己写一个/login/loginUser的接口, //但其实这里是交给SpringSecurity自己去检验的,默认情况下只需要携带form-data类型的账号密码提交即可。 //本项目将会对默认请求进行重写,使用存在redis中的验证码验证 .loginProcessingUrl("/login/loginUser") //设置登录参数别名 //SpringSecurity默认情况下账号和密码的属性名为username、password。 //当然也可以跟我一样重新设置别名。(虽然设置的是一样的,orz) .usernameParameter("username") .passwordParameter("password") //登录成功后的回调,我看其他博客写的是自定义返回类,因为我并没做其他操作,就简单一点吧,看后面代码。 //为什么是HttpResponseResult::loginSuccess而其他的却是->? //因为登录成功接口我的形参和该方法形参一致,所以可以这样写 .successHandler(HttpResponseResult::loginSuccess) //登录失败回调 .failureHandler((req, resp, e) -> HttpResponseResult.loginError(resp,e)) //权限不足回调 .accessDeniedHandler(HttpResponseResult::insufficientPermissions) //自定义authenticationDetailsSource,目的是为了获取请求的验证码等信息 .authenticationDetailsSource(authenticationDetailsSource) .permitAll() .and() .csrf().disable() .exceptionHandling() .authenticationEntryPoint((req, resp, auth) -> HttpResponseResult.noLogin(resp)) .and() //设置无状态的连接,即不创建session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() //退出登录 .logout() .logoutUrl("/login/logout") .logoutSuccessHandler((req,resp,auth) ->HttpResponseResult.logout(resp)) .permitAll() .and() ;
Springsecurity默认不携带验证码进行验证,所以这里我们需要重写相关配置类,一会请看代码 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //将自定义的Provider装配到Builder auth.authenticationProvider(loginAuthenticationProvider); //将自定义的loginserviceimpl装配到builder auth.userDetailsService(loginUserDetails).passwordEncoder(new PasswordEncoder() { @Override public String encode(CharSequence rawPassword) { return rawPassword.toString(); }
@Overridepublicbooleanmatches(CharSequencerawPassword,StringencodedPassword){returnencodedPassword.equals(rawPassword.toString());}
}); }
静态资源放行 */ @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/js/", "/css/","/images/**"); }
角色继承,比如在一个系统中admin属于最高角色"超级管理员",那么他将拥有其他角色所有的权限
以>来设置
admin > user > normal > ......
@return */ @Bean RoleHierarchy roleHierarchy(){ RoleHierarchyImpl hierarchy = new RoleHierarchyImpl(); hierarchy.setHierarchy("ROLE_admin > ROLE_user"); return hierarchy; }
注入自定义jwttoken过滤器 */ @Bean protected JwtAuthenticationTokenFilter authenticationTokenFilter() throws Exception{ return new JwtAuthenticationTokenFilter(); }
SpringSecurity5.X要求必须指定密码加密方式,否则会在请求认证的时候报错
同样的,如果指定了加密方式,就必须您的密码在数据库中存储的是加密后的,才能比对成功
@return */ @Bean protected BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
//使用自定义的jwttoken过滤器来进行验证//randomCode是一个时间戳,由前端生成后请求后端,具体是防止redis中的key重复@ApiOperation(value="验证码",notes="获取验证码")@GetMapping("/getRandomCode")publicResultgetRandomCode(@RequestParamStringrandomCode){if(ObjectUtil.isEmpty(randomCode)){returnResult.error(500,"请输入验证码!");}returnResult.successData(loginService.getRandomCode(randomCode));}0 http.addFilterBefore(authenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class); //禁止页面缓存 http.headers().cacheControl(); }
}
###编写SpringSecurity的回调返回类
/**
针对返回响应的封装
@author yueranzs
@date 2021/11/22 14:13 */ @Data public class HttpResponseResult {
/**
/**
/**
/**
/**
/**
权限不足
@param req
@param resp
@param e */ public static void insufficientPermissions(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException { data(resp,UserResultCode.USER_INSUFFICIENT_PERMISSIONS); } }
//randomCode是一个时间戳,由前端生成后请求后端,具体是防止redis中的key重复@ApiOperation(value="验证码",notes="获取验证码")@GetMapping("/getRandomCode")publicResultgetRandomCode(@RequestParamStringrandomCode){if(ObjectUtil.isEmpty(randomCode)){returnResult.error(500,"请输入验证码!");}returnResult.successData(loginService.getRandomCode(randomCode));}0
登录成功
@param req
@param resp
@param auth */ public static void loginSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException { //生成token JwtUtil jwtUtil = new JwtUtil(); Map<String, Object> user = new HashMap<>(); user.put("username",auth.getName()); //token我只包含了username,因为在下面自定义jwttoken过滤器里面会查询角色等信息 String token = jwtUtil.create(user); base(resp,new JSONObject().set("code",200).set("data",token)); }
退出
@param resp */ public static void logout(HttpServletResponse resp) throws IOException { data(resp,UserResultCode.USER_LOGOUT_SUCCESS); }
登录失败
@param resp
@param exception security的认证异常 */ public static void loginError(HttpServletResponse resp,AuthenticationException exception) throws IOException { if (exception instanceof LockedException) { //账户锁定 data(resp,UserResultCode.USER_ACCOUNT_LOCKED); } else if (exception instanceof CredentialsExpiredException) { //密码过期 data(resp,UserResultCode.USER_CREDENTIALS_EXPIRED); } else if (exception instanceof AccountExpiredException) { //账户过期 data(resp,UserResultCode.USER_ACCOUNT_EXPIRED); } else if (exception instanceof DisabledException) { //账户禁用 data(resp,UserResultCode.USER_ACCOUNT_DISABLE); } else if (exception instanceof BadCredentialsException) { //用户名或者密码输入错误 data(resp,UserResultCode.USER_LOGIN_ERROR_NO); }else if (exception instanceof InternalAuthenticationServiceException){ //用户不存在 data(resp,UserResultCode.USER_ACCOUNT_NOT_EXIST); } }
暂无凭证或是认证失败
@param resp */ public static void noProof(HttpServletResponse resp) throws IOException { data(resp,UserResultCode.USER_NOT_PROOF); }
基础返回
@param resp
@param jsonObject
@throws IOException */ public static void base(HttpServletResponse resp,JSONObject jsonObject) throws IOException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.println(jsonObject); out.flush(); out.close(); } /**
响应返回封装
@param resp
@param resultCode
@return */ public static void data(HttpServletResponse resp,CustomizeResultCode resultCode) throws IOException { JSONObject result = new JSONObject(); result.set("code",resultCode.getCode()); result.set("message",resultCode.getMessage()); base(resp,result); }
User类(pojo)
注意,下面用户表是mybatis-plus生成,如果想通过SpringSecurity验证,需要实现UserDetails
/***<p>*用户表*</p>**@authoryueranzs*@since2021-11-04*/@Data@EqualsAndHashCode(callSuper=false)@TableName("tb_user")@ApiModel(value="User对象",description="用户表")publicclassUserimplementsSerializable,UserDetails{privatestaticfinallongserialVersionUID=1L;@ApiModelProperty(value="用户ID")@TableId(value="id",type=IdType.AUTO)privateLongid;@ApiModelProperty(value="用户名")privateStringusername;@ApiModelProperty(value="昵称")privateStringnickname;@ApiModelProperty(value="邮箱")privateStringemail;@ApiModelProperty(value="头像")privateStringavatar;@ApiModelProperty(value="头像临时签名")@TableField(exist=false)privateStringavatarUrl;@ApiModelProperty(value="联系电话")privateStringphoneNumber;@ApiModelProperty(value="状态0锁定1有效")privateIntegerstatus;@ApiModelProperty(value="创建时间")privateDatecreateTime;@ApiModelProperty(value="修改时间")privateDatemodifiedTime;@ApiModelProperty(value="性别0男1女2保密")privateIntegersex;@ApiModelProperty(value="盐")privateStringsalt;@ApiModelProperty(value="0:超级管理员,1:系统用户")privateIntegertype;@ApiModelProperty(value="密码")privateStringpassword;@ApiModelProperty(value="生日")privateDatebirth;@ApiModelProperty(value="部门id")privateLongdepartmentId;@ApiModelProperty(value="逻辑删除")privateIntegerdeleted;@ApiModelProperty(value="角色信息")//mybatis-plus中的注解,即在对数据库操作时忽略本字段@TableField(exist=false)privateSet<?extendsGrantedAuthority>authorities;@OverridepublicbooleanisAccountNonExpired(){returntrue;}@OverridepublicbooleanisAccountNonLocked(){returntrue;}@OverridepublicbooleanisCredentialsNonExpired(){returntrue;}@OverridepublicbooleanisEnabled(){returntrue;}}
编写LoginAuthenticationDetailsSource类
/***描述:自定义AuthenticationDetailsSource,将HttpServletRequest注入到AuthenticationDetails,使其能获取到请求中的验证码等其他信息*@authoryueranzs*@date2021/12/19:42*/@ComponentpublicclassLoginAuthenticationDetailsSourceimplementsAuthenticationDetailsSource<HttpServletRequest,WebAuthenticationDetails>{@OverridepublicWebAuthenticationDetailsbuildDetails(HttpServletRequestrequest){returnnewLoginWebAuthenticationDetails(request);}}
编写LoginWebAuthenticationDetails类
/***描述:自定义WebAuthenticationDetails,将验证码和用户名、密码一同带入AuthenticationProvider中*@authoryueranzs*@date2021/12/19:38*/publicclassLoginWebAuthenticationDetailsextendsWebAuthenticationDetails{privatestaticfinallongserialVersionUID=6975601077710753878L;/*验证码value*/privatefinalStringcode;/*验证码key*/privatefinalStringrandomCode;publicLoginWebAuthenticationDetails(HttpServletRequestrequest){super(request);//这里的code是指验证码真实code,即redis中的验证码value,可自行修改成自己项目的属性名code=request.getParameter("code");//redis中的验证码key,可自行修改成自己项目的属性名randomCode=request.getParameter("randomCode");}publicStringgetRandomCode(){returnrandomCode;}publicStringgetCode(){returncode;}@OverridepublicStringtoString(){StringBuildersb=newStringBuilder();sb.append(super.toString()).append(";code:").append(this.getCode());sb.append(super.toString()).append(";randomCode:").append(this.getRandomCode());returnsb.toString();}}
编写LoginUserDetails类
/***@authoryueranzs*@date2021/12/111:38*/@ComponentpublicclassLoginUserDetailsimplementsUserDetailsService{@AutowiredprivateUserServiceuserService;/***这里是根据username(账号)去查询数据库,然后进行检验*/@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{//mybatis-plus的语句,意思是查询单个的用户根据用户名(username)和伪删除(delflag)来查Useruser=userService.getOne(newQueryWrapper<User>().lambda().select(User::getId,User::getUsername,User::getPassword).eq(User::getUsername,username).eq(User::getDeleted,ResultCode.NODELETE.getCode()));if(ObjectUtil.isNull(user)){//用户不存在,抛出SpringSecurity异常thrownewInternalAuthenticationServiceException(UserResultCode.USER_ACCOUNT_NOT_EXIST.getMessage());}//查询角色List<Role>roles=userService.getRolesByUserId(user.getId());Setauthorities=newHashSet();//注意:SpringSecurity授权分两种:角色和权限//角色授权:在授权时,前缀必须加上"ROLE_",一般使用AuthorityUtils.commaSeparatedStringToAuthorityList(字符串,用逗号添加多个role)//AuthorityUtils.commaSeparatedStringToAuthorityList就不需要自己加"ROLE_"了//权限授权:不需要加前缀//后面的hasRole和hasAuthority千万不要搞错了,Role是角色,Authority是权限,我当初就是看错了,找了很久的问题,后面看代码roles.forEach(role->authorities.add(newSimpleGrantedAuthority("ROLE_"+role.getRoleName())));//千万要记得查询到角色信息后记得设置user.setAuthorities(authorities);//返回returnuser;}}
编写LoginAuthenticationProvider类
/***描述:自定义SpringSecurity的认证器*@authoryueranzs*@date2021/12/19:44*/@ComponentpublicclassLoginAuthenticationProviderextendsAbstractUserDetailsAuthenticationProvider{@AutowiredprivateLoginUserDetailsloginUserDetails;@OverrideprotectedvoidadditionalAuthenticationChecks(UserDetailsuserDetails,UsernamePasswordAuthenticationTokenauthentication)throwsAuthenticationException{}@OverrideprotectedUserDetailsretrieveUser(Stringusername,UsernamePasswordAuthenticationTokenauthentication)throwsAuthenticationException{returnnull;}@OverridepublicAuthenticationauthenticate(Authenticationauthentication)throwsAuthenticationException{//用户名Stringusername=authentication.getName();//密码Stringpassword=authentication.getCredentials().toString();LoginWebAuthenticationDetailsloginWebAuthenticationDetails=(LoginWebAuthenticationDetails)authentication.getDetails();//验证码keyStringrandomCode=loginWebAuthenticationDetails.getRandomCode();//验证码valueStringcode=loginWebAuthenticationDetails.getCode();//验证码是否为空if(ObjectUtil.isEmpty(randomCode)||ObjectUtil.isEmpty(code)){thrownewNullPointerException("请输入验证码");}//检验验证码是否正确if(!validateVerifyRandomCode(randomCode,code)){thrownewBusinessException(UserResultCode.REDIS_CODE.getCode(),UserResultCode.REDIS_CODE.getMessage());}Useruser=(User)loginUserDetails.loadUserByUsername(username);//密码是否一致if(!user.getPassword().equals(SecureUtil.md5(password))){//密码错误,不过因为安全性的问题所以返回此异常,意思是用户名或者密码错误thrownewBadCredentialsException(UserResultCode.USER_CREDENTIALS_ERROR.getMessage());}//删除redis的验证码RedisUtil.del("code_"+randomCode);returnthis.createSuccessAuthentication(user,authentication,user);}/***验证用户输入的验证码*@paramrandomCode验证码key*@paramcode验证码value*@return*/publicbooleanvalidateVerifyRandomCode(StringrandomCode,Stringcode){//验证码是否一致ObjectredisCode=RedisUtil.get("code_"+randomCode);returnObjectUtil.equals(code,redisCode);}}
编写Jwt配置类
/***jwt配置类*@authoryueranzs*@date2021/12/49:57*/@Data@ToString@Configuration//与配置文件中的数据关联起来(这个注解会默认自动匹配jwt开头的配置)@ConfigurationProperties(prefix="jwt")publicclassJwtConfig{/*requestHeaders:Authorization*/privateStringheader;/*Base64对该令牌进行编码*/privateStringbase64Secret;/*令牌过期时间此处单位/毫秒*/privateLongtokenValidityInSeconds;}
JwtUtil工具类
注意,本工具类建立在hutool工具类的基础上,仅供参考,部分属性值请视自己情况定
/***jwt工具类*@authoryueranzs*@date2021/11/2515:55*/@ComponentpublicclassJwtUtil{privatestaticJwtConfigjwtConfig;@AutowiredprivatevoidsetJwtConfig(JwtConfigjwtConfig){JwtUtil.jwtConfig=jwtConfig;}/***生成jwt*@parampayload数据主体*@return*/publicStringcreate(Map<String,Object>payload){//每个jwt都默认生成一个到期时间payload.put("expire_time",DateUtil.current()+jwtConfig.getTokenValidityInSeconds());//生成私钥JWTSignerjwtSigner=JWTSignerUtil.hs256(jwtConfig.getBase64Secret().getBytes(StandardCharsets.UTF_8));//生成tokenreturnJWTUtil.createToken(payload,jwtSigner);}/***解析jwt*@paramtoken*@return*/publicJSONObjectparse(Stringtoken){returnJWTUtil.parseToken(token).getPayload().getClaimsJson();}/***校验token是否正确*@paramtoken*@return*/publicbooleanverifyToken(Stringtoken){//先判断是否到期,再判断是否正确if(expiredToken(token)){returnJWTUtil.verify(token,jwtConfig.getBase64Secret().getBytes(StandardCharsets.UTF_8));}returnfalse;}/***校验token是否过期*@paramtoken*@return*/publicbooleanexpiredToken(Stringtoken){returnDateUtil.current()<getExpiredToken(token);}/***获取token过期时间*@paramtoken*@return*/publiclonggetExpiredToken(Stringtoken){returnLong.parseLong(parse(token).get("expire_time").toString());}/***获取登录人账号*@paramtoken*@return*/publicStringgetUserNameToken(Stringtoken){returnparse(token).get("username").toString();}/***获取登录人角色集合*@paramtoken*@return*/publicSet<GrantedAuthority>getRolesToken(Stringtoken){return(Set<GrantedAuthority>)parse(token).get("authorities");}}
applicaiton.yml中进行追加jwt信息
//randomCode是一个时间戳,由前端生成后请求后端,具体是防止redis中的key重复@ApiOperation(value="验证码",notes="获取验证码")@GetMapping("/getRandomCode")publicResultgetRandomCode(@RequestParamStringrandomCode){if(ObjectUtil.isEmpty(randomCode)){returnResult.error(500,"请输入验证码!");}returnResult.successData(loginService.getRandomCode(randomCode));}8
编写JwtAuthenticationTokenFilter过滤类
/***jwttokenfilter*@authoryueranzs*@date2021/12/410:14*/@Slf4j@ComponentpublicclassJwtAuthenticationTokenFilterextendsOncePerRequestFilter{@ResourceprivateUserDetailsServiceuserDetailsService;@ResourceprivateJwtUtiljwtUtil;@ResourceprivateJwtConfigjwtConfig;@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{StringrequestUrl=request.getRequestURI();StringauthToken=request.getHeader(jwtConfig.getHeader());StringuserName=null;if(ObjectUtil.isNotEmpty(authToken)){userName=jwtUtil.getUserNameToken(authToken);}log.info("进入jwt自定义token过滤器");log.info("自定义token过滤器获得用户名为:"+userName);//当userName不为空时进行校验token是否为有效token//ObjectUtil.isNotEmpty()和ObjectUtil.isNull()是hutool中的方法。/*前者意思是指对象是否不为空,和isNotNull()不同。比如"",isNotNull()会返回true而isNotEmpty()会返回false。userName是字符串所以使用isNotEmpty(),该方法也很适合集合判空*//*getAuthentication()使用isNull()原因是:通过前面几个代码块的代码,可以看出是存储授权信息的这里的意思是如果用户名不为空并且授权信息又有值,那么就直接跳过,反之就是进入下面的if内部*/if(ObjectUtil.isNotEmpty(userName)&&ObjectUtil.isNull(SecurityContextHolder.getContext().getAuthentication())){UserDetailsuserDetails=this.userDetailsService.loadUserByUsername(userName);Useruser=(User)userDetails;//检验tokenif(!jwtUtil.verifyToken(authToken)){thrownewBusinessException(500,"token已过期");}elseif(StrUtil.equals(userName,user.getUsername())){/***UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication*所以当在页面中输入用户名和密码之后首先会进入到UsernamePasswordAuthenticationToken验证(Authentication),*然后生成的Authentication会被交由AuthenticationManager来进行管理*而AuthenticationManager管理一系列的AuthenticationProvider,*而每一个Provider都会通UserDetailsService和UserDetail来返回一个*以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication*/UsernamePasswordAuthenticationTokenauthentication=newUsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());authentication.setDetails(newWebAuthenticationDetailsSource().buildDetails(request));//将authentication放入SecurityContextHolder中SecurityContextHolder.getContext().setAuthentication(authentication);}}filterChain.doFilter(request,response);}}
hasRole、hasAuthority
关于这两只的区别可以看链接:Spring Security 中的 hasRole 和 hasAuthority 有区别吗? - 云+社区 - 腾讯云 (tencent.com)
/***前面代码块中我说过这两个注解千万不要混淆,虽然在使用上,都并不需要加前缀*但我之前没注意清楚,在给用户授权时我写了ROLE_admin,但是使用的是hasAuthority*也就导致我怎么都访问不了这个方法,后面半信半疑hasAuthority('ROLE_admin')才能访问*再后来发现是自己用错方法了,换上hasRole('admin')就没问题**@PreAuthorize可以看我第一个分享的链接*hashRole和hasAuthority在springsecurity4的时候才有了ROLE_前缀区分,早期几乎是一模一样的*@return*/@PreAuthorize("hasAuthority('admin')")@ApiOperation(value="测试一下",notes="测试一下")@GetMapping("/test")publicResulttest(){returnResult.successData("hahah");}
运行效果
登录成功
登录失败
token过期
暂无权限
ps:其他的一些状态码暂未测试,目前这些也已足以,后续如有其他需要补充的我会再来添代码。就先这样吧。谢谢阅读。