SpringBoot-零基础搭建前后端分离–后端搭建
1.创建父项目verse
点击
Create New Project
选择 Maven ,选择本地安装的JDK, 点击 Next
输入GroupID: com.verse 、ArtiactID:verse 点击 Finish
创建完成后,删除
src
在
pom.xml
中添加依赖管理
<projectxmlns="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.0modelVersion><groupId>com.versegroupId><artifactId>verseartifactId><version>1.0.0version><description>前后端分离versedescription><properties><java.version>1.8java.version><project.build.sourceEncoding>UTF-8project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding><maven.compiler.source>1.8maven.compiler.source><maven.compiler.target>1.8maven.compiler.target><spring.parent>2.5.13spring.parent><mybatis-plus-version>3.5.1mybatis-plus-version><hutool.all.version>5.5.7hutool.all.version><swagger.version>3.0.0swagger.version>properties><modules>modules><dependencyManagement><dependencies><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-parentartifactId><version>${spring.parent}version><type>pomtype><scope>importscope>dependency><dependency><groupId>com.baomidougroupId><artifactId>mybatis-plus-boot-starterartifactId><version>${mybatis-plus-version}version>dependency><dependency><groupId>cn.hutoolgroupId><artifactId>hutool-allartifactId><version>${hutool.all.version}version>dependency><dependency><groupId>io.springfoxgroupId><artifactId>springfox-boot-starterartifactId><version>${swagger.version}version>dependency>dependencies>dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.pluginsgroupId><artifactId>maven-compiler-pluginartifactId><configuration><source>${maven.compiler.source}source><target>${maven.compiler.target}target>configuration>plugin>plugins>build>project>
2.创建verse-commons
概述
通用异常处理以及通用响应数据结构等内容
新建verse-commons Module
右击 verse模块名,点击 New > Module
选择 Maven ,选择本地安装的JDK, 点击 Next
输入GroupID:
com.verse.commons
、ArtiactID:verse-commons
点击 Finish
修改
verse-commons
的pom.xml
<projectxmlns="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"><parent><artifactId>verseartifactId><groupId>com.versegroupId><version>1.0.0version>parent><modelVersion>4.0.0modelVersion><groupId>com.verse.commonsgroupId><artifactId>verse-commonsartifactId><dependencies><dependency><groupId>org.projectlombokgroupId><artifactId>lombokartifactId><optional>trueoptional><scope>providedscope>dependency><dependency><groupId>com.fasterxml.jackson.coregroupId><artifactId>jackson-annotationsartifactId>dependency><dependency><groupId>org.springframeworkgroupId><artifactId>spring-webartifactId><scope>providedscope>dependency>dependencies>project>
添加统一返回结果
创建返回码接口IResultCode
/*** 返回码接口*/publicinterfaceIResultCode{/*** 返回码**@returnint*/intgetCode();/*** 返回消息**@returnString*/StringgetMsg();}
创建返回接口码的实现类ResultCode
packagecom.verse.commons.api;importlombok.AllArgsConstructor;importlombok.Getter;/*** 返回码实现*/@Getter@AllArgsConstructorpublicenumResultCode implements IResultCode {/*** 操作成功*/SUCCESS(200,"操作成功"),/*** 业务异常*/FAILURE(400,"业务异常"),/*** 业务异常*/Unauthorized(401,"用户、密码输入错误"),/*** 服务未找到*/NOT_FOUND(404,"服务未找到"),/*** 服务异常*/ERROR(500,"服务异常"),USER_INPUT_ERROR(400,"您输入的数据格式错误或您没有权限访问资源!"),/*** Too Many Requests*/TOO_MANY_REQUESTS(429,"Too Many Requests");/*** 状态码*/finalintcode;/*** 消息内容*/finalString msg;}
创建 统一响应消息报文Result类
/*** 统一响应消息报文*@param*/ @Data@GetterpublicclassResult<T>implementsSerializable{privatestaticfinallongserialVersionUID =1L;privateintcode;privateString msg;privatelongtime;@JsonInclude(JsonInclude.Include.NON_NULL)privateT data;privateResult(){this.time = System.currentTimeMillis();}privateResult(IResultCode resultCode){this(resultCode,null, resultCode.getMsg());}privateResult(IResultCode resultCode, String msg){this(resultCode,null, msg);}privateResult(IResultCode resultCode, T data){this(resultCode, data, resultCode.getMsg());}privateResult(IResultCode resultCode, T data, String msg){this(resultCode.getCode(), data, msg);}privateResult(intcode, T data, String msg){this.code = code;this.data = data;this.msg = msg;this.time = System.currentTimeMillis();}/*** 返回状态码**@paramresultCode 状态码*@param 泛型标识*@returnApiResult*/ publicstaticResultsuccess(IResultCode resultCode) {returnnewResult<>(resultCode);}publicstaticResultsuccess(String msg) {returnnewResult<>(ResultCode.SUCCESS, msg);}publicstaticResultsuccess(IResultCode resultCode, String msg) {returnnewResult<>(resultCode, msg);}publicstaticResultdata(T data) {returndata(data, VerseConstant.DEFAULT_SUCCESS_MESSAGE);}publicstaticResultdata(T data, String msg) {returndata(ResultCode.SUCCESS.code, data, msg);}publicstaticResultdata(intcode, T data, String msg) {returnnewResult<>(code, data, data ==null? VerseConstant.DEFAULT_NULL_MESSAGE : msg);}publicstaticResultfail() {returnnewResult<>(ResultCode.FAILURE, ResultCode.FAILURE.getMsg());}publicstaticResultfail(String msg) {returnnewResult<>(ResultCode.FAILURE, msg);}publicstaticResultfail(intcode, String msg) {returnnewResult<>(code,null, msg);}publicstaticResultfail(IResultCode resultCode) {returnnewResult<>(resultCode);}publicstaticResultfail(IResultCode resultCode, String msg) {returnnewResult<>(resultCode, msg);}publicstaticResultcondition(booleanflag) {returnflag ? success(VerseConstant.DEFAULT_SUCCESS_MESSAGE) : fail(VerseConstant.DEFAULT_FAIL_MESSAGE);}}
创建基础异常处理类BaseException
packagecom.verse.commons.exception;importcom.verse.commons.api.ResultCode;importlombok.Data;importorg.springframework.http.HttpStatus;importstaticorg.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;/*** 基础异常处理类*/@DatapublicclassBaseExceptionextendsRuntimeException{privatestaticfinallongserialVersionUID =5782968730281544562L;privateintstatus = INTERNAL_SERVER_ERROR.value();publicBaseException(String message){super(message);}publicBaseException(HttpStatus status, String message){super(message);this.status = status.value();}publicBaseException(intcode, String message){super(message);this.code = code;this.message =message;}//异常错误编码privateintcode ;//异常信息privateString message;privateBaseException(){}publicBaseException(ResultCode resultCode){this.code = resultCode.getCode();this.message = resultCode.getMsg();}}
创建verse基本常量
/*** verse基本常量*/publicclassVerseConstant{/*** 默认成功消息*/publicstaticfinalString DEFAULT_SUCCESS_MESSAGE ="处理成功";/*** 默认失败消息*/publicstaticfinalString DEFAULT_FAIL_MESSAGE ="处理失败";/*** 默认为空消息*/publicstaticfinalString DEFAULT_NULL_MESSAGE ="承载数据为空";}
3.创建verse-jwt
概述
项目的后端核心服务(Spring Boot web应用)
新建verse-jwt Module
右击 verse模块名,点击 New > Module
选择 Maven ,选择本地安装的JDK, 点击 Next
输入GroupID:
com.verse.jwt
、ArtiactID:verse-jwt
点击 Finish
修改`verse-jwt的pom.xml
<projectxmlns="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"><parent><artifactId>verseartifactId><groupId>com.versegroupId><version>1.0.0version>parent><modelVersion>4.0.0modelVersion><groupId>com.verse.jwtgroupId><artifactId>verse-jwtartifactId><dependencies><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-webartifactId>dependency><dependency><groupId>com.verse.commonsgroupId><artifactId>verse-commonsartifactId><version>1.0.0version>dependency><dependency><groupId>mysqlgroupId><artifactId>mysql-connector-javaartifactId><scope>runtimescope>dependency><dependency><groupId>com.baomidougroupId><artifactId>mybatis-plus-boot-starterartifactId>dependency><dependency><groupId>com.baomidougroupId><artifactId>mybatis-plus-generatorartifactId>dependency><dependency><groupId>org.apache.velocitygroupId><artifactId>velocityartifactId>dependency><dependency><groupId>io.springfoxgroupId><artifactId>springfox-boot-starterartifactId>dependency>dependencies>project>
创建verse-jwt的入口类VerseJwtApplication
@MapperScan("com.verse.jwt.*.mapper")@SpringBootApplicationpublicclassVerseJwtApplication{publicstaticvoidmain(String[] args){SpringApplication.run(VerseJwtApplication.class,args);}}
4.在verse-jwt中实现代码生成
参考mybatis-plus
代码生成:https://baomidou.com/pages/779a6e/
在verse-jwt中的pom.xml添加依赖
<dependency><groupId>com.baomidougroupId><artifactId>mybatis-plus-generatorartifactId>dependency>
创建MybatisPlusGenerator类
DATA_SOURCE_CONFIG:数据链接配置
generator:根据模板生成代码
getTables方法:数据库表,all表示库中所有表
将模板添加到
src\main\resources\templates
文件下
publicclassMybatisPlusGenerator{privatestaticfinalDataSourceConfig.Builder DATA_SOURCE_CONFIG =newDataSourceConfig.Builder("jdbc:mysql://localhost:3306/verse?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai","root","root");publicstaticvoidgenerator(){FastAutoGenerator.create(DATA_SOURCE_CONFIG)// 全局配置.globalConfig(builder -> {builder.author("springboot葵花宝典")// 设置作者.enableSwagger()// 开启 swagger 模式.fileOverride()// 覆盖已生成文件.outputDir("D:\\software\\file\\verse");// 指定输出目录})// 包配置.packageConfig((scanner, builder) -> builder.parent(scanner.apply("请输入包名?")).pathInfo(Collections.singletonMap(OutputFile.xml,"D:\\software\\file\\verse\\mapper")))// 策略配置.strategyConfig((scanner, builder) -> builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?所有输入 all"))).controllerBuilder().enableRestStyle().enableHyphenStyle().entityBuilder().enableLombok()// .addTableFills(// new Column("create_by", FieldFill.INSERT),// new Column("create_time", FieldFill.INSERT),// new Column("update_by", FieldFill.INSERT_UPDATE),// new Column("update_time", FieldFill.INSERT_UPDATE)// ).build())/*模板引擎配置,默认 Velocity 可选模板引擎 Beetl 或 Freemarker.templateEngine(new BeetlTemplateEngine()).templateEngine(new FreemarkerTemplateEngine())*/.execute();}// 处理 all 情况protectedstaticListgetTables(String tables) {return"all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));}}
创建代码生成入口类GeneratorMain
publicclassGeneratorMain{publicstaticvoidmain(String[] args){MybatisPlusGenerator.generator();}}
运行GeneratorMain
生成结果如下
将system包放在verse-jwt
的com.verse.jwt包下,结果如下:
将mapper放在verse-jwt
的src\main\resources
包下,结果如下:
5.整合Swagger-ui实现在线API文档
添加项目依赖
在
verse-jwt
项目的pom.xml中新增Swagger-UI相关依赖
<dependency><groupId>io.springfoxgroupId><artifactId>springfox-boot-starterartifactId>dependency>
添加Swagger-UI的配置
在verse-jwt
项目中添加如下类Swagger2Config
:
packagecom.verse.jwt.config;importorg.springframework.beans.BeansException;importorg.springframework.beans.factory.config.BeanPostProcessor;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.ReflectionUtils;importorg.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;importspringfox.documentation.builders.ApiInfoBuilder;importspringfox.documentation.builders.PathSelectors;importspringfox.documentation.builders.RequestHandlerSelectors;importspringfox.documentation.service.*;importspringfox.documentation.spi.DocumentationType;importspringfox.documentation.spi.service.contexts.SecurityContext;importspringfox.documentation.spring.web.plugins.Docket;importspringfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;importspringfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;importjava.lang.reflect.Field;importjava.util.ArrayList;importjava.util.List;importjava.util.stream.Collectors;@ConfigurationpublicclassSwagger2Config{@BeanpublicDocketcreateRestApi(){returnnewDocket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.verse.jwt.system.controller")).paths(PathSelectors.any()).build();}privateApiInfoapiInfo(){returnnewApiInfoBuilder().title("SwaggerUI演示").description("verse").contact(newContact("springboot葵花宝典",null,null)).version("1.0").build();}@BeanpublicstaticBeanPostProcessorspringfoxHandlerProviderBeanPostProcessor(){returnnewBeanPostProcessor() {@OverridepublicObjectpostProcessAfterInitialization(Object bean, String beanName)throwsBeansException{if(beaninstanceofWebMvcRequestHandlerProvider || beaninstanceofWebFluxRequestHandlerProvider) {customizeSpringfoxHandlerMappings(getHandlerMappings(bean));}returnbean;}privatevoidcustomizeSpringfoxHandlerMappings(List mappings) {List copy = mappings.stream().filter(mapping -> mapping.getPatternParser() ==null).collect(Collectors.toList());mappings.clear();mappings.addAll(copy);}@SuppressWarnings("unchecked")privateListgetHandlerMappings(Object bean) {try{Field field = ReflectionUtils.findField(bean.getClass(),"handlerMappings");field.setAccessible(true);return(List) field.get(bean);}catch(IllegalArgumentException | IllegalAccessException e) {thrownewIllegalStateException(e);}}};}}
修改配置
修改application.yml
文件,MVC默认的路径匹配策略为PATH_PATTERN_PARSER
,需要修改为ANT_PATH_MATCHER
;
spring:jackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: GMT+8datasource:url: jdbc:mysql://localhost:3306/verse?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus:config-location:mapper-locations:- classpath:mapper/*.xml- classpath*:com/**/mapper/*.xmlconfiguration:map-underscore-to-camel-case:truespringfox:documentation:enabled:trueserver:port: 8888
添加一个测试接口
在ISysUserService接口中添加方法
importcom.verse.jwt.system.entity.SysUser;importcom.baomidou.mybatisplus.extension.service.IService;/*** * 用户信息表 服务类*
**@authorspringboot葵花宝典*@since2022-04-27*/publicinterfaceISysUserServiceextendsIService<SysUser>{SysUsergetUserByUserName(String userName);}
在SysUserServiceImpl中实现
importcn.hutool.core.util.StrUtil;importcom.baomidou.mybatisplus.core.conditions.query.QueryWrapper;importcom.verse.jwt.system.entity.SysUser;importcom.verse.jwt.system.mapper.SysUserMapper;importcom.verse.jwt.system.service.ISysUserService;importcom.baomidou.mybatisplus.extension.service.impl.ServiceImpl;importorg.springframework.stereotype.Service;importorg.springframework.util.Assert;importjavax.annotation.Resource;/*** * 用户信息表 服务实现类*
**@authorspringboot葵花宝典*@since2022-04-27*/@ServicepublicclassSysUserServiceImplextendsServiceImpl<SysUserMapper,SysUser>implementsISysUserService{@ResourceprivateSysUserMapper sysUserMapper;/*** 根据登录用户名查询用户信息*@paramuserName 用户信息*@return*/@OverridepublicSysUsergetUserByUserName(String userName){Assert.isTrue(StrUtil.isNotEmpty(userName),"查询参数用户名不存在");SysUser sysUser = sysUserMapper.selectOne(newQueryWrapper().eq("username",userName));if(sysUser !=null){sysUser.setPassword("");//清空密码信息}returnsysUser;}}
在SysUserController中实现接口
importcom.verse.jwt.system.entity.SysUser;importcom.verse.jwt.system.service.ISysUserService;importio.swagger.annotations.ApiOperation;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importio.swagger.annotations.Api;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;importjavax.annotation.Resource;/*** * 用户信息表 前端控制器*
**@authorspringboot葵花宝典*@since2022-04-27*/@RestController@Api(tags ="SysUserController", description =" 用户信息表")@RequestMapping("/sys-user")publicclassSysUserController{@ResourceprivateISysUserService sysuserService;/*** 根据登录用户名查询用户信息*@paramusername 用户名称*@return*/@ApiOperation(value ="info")@GetMapping(value ="/info")publicSysUserinfo(@RequestParam("username")String username){returnsysuserService.getUserByUserName(username);}}
测试
启动项目后,访问http://localhost:8888/swagger-ui/地址,结果如下
6.整合SpringSecurity和JWT实现认证和授权
项目使用表说明
sys_user是用户信息表,用于存储用户的基本信息,如:用户名、密码
sys_role是角色信息表,用于存储系统内所有的角色
sys_menu是系统的菜单信息表,用于存储系统内所有的菜单。用id与父id的字段关系维护一个菜单树形结构。
sys_user_role是用户角色多对多关系表,一条userid与roleid的关系记录表示该用户具有该角色,该角色包含该用户。
sys_role_menu是角色菜单(权限)关系表,一条roleid与menuid的关系记录表示该角色由某菜单权限,该菜单权限可以被某角色访问。
sys_api,用于存储可以被访问的资源服务接口
sys_role_api,一条roleid与apiid的关系记录表示该角色具有某个api接口的访问权限。
添加项目依赖
在
verse-jtw
的pom.xml中添加security
项目依赖
<dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-securityartifactId>dependency><dependency><groupId>io.jsonwebtokengroupId><artifactId>jjwtartifactId>dependency>
添加Jwt属性配置类
JwtProperties属性类
/*** jwt配置的属性*/@Data@Component@ConfigurationProperties("verse.jwt")publicclassJwtProperties{//是否开启JWT,即注入相关的类对象privateBoolean enabled;//JWT密钥privateString secret;//JWT有效时间privateLong expiration;//前端向后端传递JWT时使用HTTP的header名称privateString header;//用户获取JWT令牌发送的用户名参数名称privateString userParamName ="username";//用户获取JWT令牌发送的密码参数名称privateString pwdParamName ="password";//允许哪些域对本服务的跨域请求privateList corsAllowedOrigins;//允许哪些HTTP方法跨域privateList corsAllowedMethods;//是否关闭csrf跨站攻击防御功能privateBoolean csrfDisabled =true;//是否使用默认的JWTAuthControllerprivateBoolean useDefaultController =true;}
VerseApiProperties属性类
VerseApiProperties权限全面开放的接口,不需要JWT令牌就可以访问,或者开发过程临时开放的URI
/*** 权限全面开放的接口,不需要JWT令牌就可以访问,或者开发过程临时开放的URI*/@Data@Component@ConfigurationProperties(prefix ="verse.uaa")publicclassVerseApiProperties{/*** 监控中心和swagger需要访问的url*/publicstaticfinalString[] ENDPOINTS = {"/jwtauth/**","/swagger-ui/swagger-resources/**","/swagger-resources/**","/webjars/**","/swagger-ui/**","/v2/api-docs","/v3/api-docs",};/*** 忽略URL,List列表形式*/privateList ignoreUrl =newArrayList<>();/*** 首次加载合并ENDPOINTS*/@PostConstructpublicvoidinitIgnoreUrl(){Collections.addAll(ignoreUrl, ENDPOINTS);}}
在
application.yml
添加注解
verse:jwt:enabled:truesecret:verseexpiration:3600000header:JWTHeaderNameuserParamName:usernamepwdParamName:passwordcorsAllowedOrigins:-http://localhost:8080-http://127.0.0.1:8080corsAllowedMethods:-GET-POSTuseDefaultController:trueuaa:ignoreUrl:权限全面开放的接口,不需要JWT令牌就可以访问,或者开发过程临时开放的URI-/sys-user/info根据用户名获取用户信息
添加JWT token的工具类
添加JWT token的工具类用于生成和解析JWT token的工具类
相关方法说明:
String generateToken(String username,Map
payloads) :用于根据登录用户信息生成token String getUsernameFromToken(String token):从token中获取登录用户的信息
Boolean validateToken(String token, String usernameParam):判断token是否还有效
/*** JwtToken生成工具类*/@Slf4j@ComponentpublicclassJwtTokenUtil{@AutowiredprivateJwtProperties jwtProperties;privatestaticfinalString CLAIM_KEY_CREATED ="created";/*** 根据用户信息生成JWT的token令牌**@paramusername 用户*@parampayloads 令牌中携带的附加信息*@return令token牌*/publicStringgenerateToken(String username,Map payloads) {intpayloadSizes = payloads ==null?0: payloads.size();Map claims =newHashMap<>(payloadSizes +2);claims.put("sub", username);claims.put("created",newDate());if(payloadSizes >0){for(Map.Entry entry:payloads.entrySet()){claims.put(entry.getKey(),entry.getValue());}}returngenerateToken(claims);}/*** 从token中获取JWT中的负载**@paramtoken 令牌*@return数据声明*/privateClaimsgetClaimsFromToken(String token){Claims claims;try{claims = Jwts.parser().setSigningKey(jwtProperties.getSecret()).parseClaimsJws(token).getBody();}catch(Exception e) {claims =null;}returnclaims;}/*** 生成token的过期时间*/privateDategenerateExpirationDate(){returnnewDate(System.currentTimeMillis() + + jwtProperties.getExpiration());}/*** 从token令牌中获取登录用户名**@paramtoken 令牌*@return用户名*/publicStringgetUsernameFromToken(String token){String username;try{Claims claims = getClaimsFromToken(token);username = claims.getSubject();}catch(Exception e) {username =null;}returnusername;}/*** 验证token是否还有效**@paramtoken 客户端传入的token令牌*@paramusernameParam 用户名的唯一标识*@return是否有效*/publicBooleanvalidateToken(String token, String usernameParam){//根据toekn获取用户名String username = getUsernameFromToken(token);return(username.equals(usernameParam) && !isTokenExpired(token));}/*** 判断令牌是否过期**@paramtoken 令牌*@return是否过期*/publicBooleanisTokenExpired(String token){try{//根据token获取获取过期时间Date expiration = getExpiredDateFromToken( token);returnexpiration.before(newDate());}catch(Exception e) {returnfalse;}}/*** 从token中获取过期时间*/privateDategetExpiredDateFromToken(String token){Claims claims = getClaimsFromToken(token);returnclaims.getExpiration();}/*** 根据负责生成JWT的token**@paramclaims 数据声明*@return令牌*/privateStringgenerateToken(Map claims) {//生成token的过期时间Date expirationDate = generateExpirationDate();returnJwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret()).compact();}/*** 刷新令牌**@paramtoken 原令牌*@return新令牌*/publicStringrefreshToken(String token){String refreshedToken;try{//获取负载信息Claims claims = getClaimsFromToken(token);claims.put(CLAIM_KEY_CREATED,newDate());refreshedToken = generateToken(claims);}catch(Exception e) {refreshedToken =null;}returnrefreshedToken;}/*** 从token令牌中获取登录用户名**@return用户名*/publicStringgetUsernameFromToken(){String username;try{RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();String jwtToken = request.getHeader(jwtProperties.getHeader());Claims claims = getClaimsFromToken(jwtToken);username = claims.getSubject();}catch(Exception e) {username =null;}returnusername;}}
添加SpringSecurity的配置类
/*** Spring Security 配置* 可以配置多个WebSecurityConfigurerAdapter* 但是多个Adaptor有执行顺序,默认值是100* 这里设置为1会优先执行*/@Configuration@Order(1)publicclassJwtSecurityConfigurerAdapterextendsWebSecurityConfigurerAdapter{@ResourceprivateJwtProperties jwtProperties;@ResourceprivateVerseApiProperties apiProperties;@ResourceprivateMyUserDetailsService myUserDetailsService;@ResourceprivateJwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@ResourceprivateRestfulAccessDeniedHandler restfulAccessDeniedHandler;@ResourceprivateRestAuthenticationEntryPoint restAuthenticationEntryPoint;@Overridepublicvoidconfigure(HttpSecurity http)throwsException{if(jwtProperties.getCsrfDisabled()){http = http.csrf().disable();}http.cors().and().sessionManagement()// 基于token,所以不需要session.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class).authorizeRequests();//通过配置实现的不需要JWT令牌就可以访问的接口for(String uri : apiProperties.getIgnoreUrl()){http.authorizeRequests().antMatchers(uri).permitAll();}//RBAC权限控制级别的接口权限校验http.authorizeRequests().anyRequest().authenticated()//.access("@rabcService.hasPermission(request,authentication)");//添加自定义未授权和未登录结果返回http.exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler).authenticationEntryPoint(restAuthenticationEntryPoint);}@Overridepublicvoidconfigure(AuthenticationManagerBuilder auth)throwsException{auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());}@Overridepublicvoidconfigure(WebSecurity web){//将项目中静态资源路径开放出来web.ignoring().antMatchers(apiProperties.ENDPOINTS);}@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}/*** 跨站资源共享配置*/@BeanCorsConfigurationSourcecorsConfigurationSource(){CorsConfiguration configuration =newCorsConfiguration();configuration.setAllowedOrigins(jwtProperties.getCorsAllowedOrigins());configuration.setAllowedMethods(jwtProperties.getCorsAllowedMethods());configuration.applyPermitDefaultValues();UrlBasedCorsConfigurationSource source =newUrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", configuration);returnsource;}@Override@Bean(name = BeanIds.AUTHENTICATION_MANAGER)publicAuthenticationManagerauthenticationManagerBean()throwsException{returnsuper.authenticationManagerBean();}@BeanpublicJwtAuthServicejwtAuthService(JwtTokenUtil jwtTokenUtil)throwsException{returnnewJwtAuthService(this.authenticationManagerBean(),jwtTokenUtil);}}
相关依赖及方法说明
corsConfigurationSource: 跨域设置
configure(HttpSecurity httpSecurity):用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器;
configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;
RestfulAccessDeniedHandler:当用户没有访问权限时的处理器,用于返回JSON格式的处理结果;
RestAuthenticationEntryPoint:当未登录或token失效时,返回JSON格式的结果;
UserDetailsService:SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;
UserDetails:SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现;
PasswordEncoder:SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder;
JwtAuthenticationTokenFilter:在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录
JwtAuthService:认证服务Service
添加RestfulAccessDeniedHandler
importcn.hutool.json.JSONUtil;importcom.verse.commons.api.Result;importorg.springframework.security.access.AccessDeniedException;importorg.springframework.security.web.access.AccessDeniedHandler;importorg.springframework.stereotype.Component;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;/*** 当访问接口没有权限时,自定义的返回结果*/@ComponentpublicclassRestfulAccessDeniedHandlerimplementsAccessDeniedHandler{@Overridepublicvoidhandle(HttpServletRequest request,HttpServletResponse response,AccessDeniedException e)throwsIOException, ServletException{response.setCharacterEncoding("UTF-8");response.setContentType("application/json");response.getWriter().println(JSONUtil.parse(Result.fail(e.getMessage())));response.getWriter().flush();}}
添加RestAuthenticationEntryPoint
importcn.hutool.json.JSONUtil;importcom.verse.commons.api.Result;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.web.AuthenticationEntryPoint;importorg.springframework.stereotype.Component;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;/*** 当未登录或者token失效访问接口时,自定义的返回结果*/@ComponentpublicclassRestAuthenticationEntryPointimplementsAuthenticationEntryPoint{@Overridepublicvoidcommence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)throwsIOException, ServletException{response.setCharacterEncoding("UTF-8");response.setContentType("application/json");response.getWriter().println(JSONUtil.parse(Result.fail(authException.getMessage())));response.getWriter().flush();}}
添加MyUserDetailsServiceMapper
importcom.verse.jwt.auth.dto.MyUserDetails;importorg.apache.ibatis.annotations.Param;importorg.apache.ibatis.annotations.Select;importjava.util.List;/*** 用户信息Mapper*/publicinterfaceMyUserDetailsServiceMapper{//根据username查询用户信息@Select("SELECT username,password,enabled\n"+"FROM sys_user u\n"+"WHERE u.username = {username}")MyUserDetailsfindByUserName(@Param("username")String username);//根据username查询用户角色列表@Select("SELECT role_code\n"+"FROM sys_role r\n"+"LEFT JOIN sys_user_role ur ON r.id = ur.role_id AND r.status = 0\n"+"LEFT JOIN sys_user u ON u.id = ur.user_id\n"+"WHERE u.username = {username}")ListfindRoleByUserName(@Param("username")String username) ;//根据用户角色查询用户菜单权限@Select({""})ListfindMenuByRoleCodes(@Param("roleCodes")List roleCodes) ;//根据用户角色查询用户接口访问权限@Select("SELECT url \n"+"FROM sys_api a \n"+"LEFT JOIN sys_role_api ra ON a.id = ra.api_id \n"+"LEFT JOIN sys_role r ON r.id = ra.role_id \n"+"WHERE r.role_code = {roleCode} \n"+"AND a.status = 0")ListfindApiByRoleCode(@Param("roleCode")String roleCode) ;}
添加MyUserDetails
publicclassMyUserDetailsimplementsUserDetails{String password;//密码String username;//用户名booleanaccountNonExpired;//是否没过期booleanaccountNonLocked;//是否没被锁定booleancredentialsNonExpired;//是否没过期booleanenabled;//账号是否可用Collection authorities;//用户的权限集合@OverridepublicCollection getAuthorities() {returnauthorities;}@OverridepublicStringgetPassword(){returnpassword;}@OverridepublicStringgetUsername(){returnusername;}@OverridepublicbooleanisAccountNonExpired(){returntrue;}@OverridepublicbooleanisAccountNonLocked(){returntrue;}@OverridepublicbooleanisCredentialsNonExpired(){returntrue;}@OverridepublicbooleanisEnabled(){returnenabled;}publicvoidsetPassword(String password){this.password = password;}publicvoidsetUsername(String username){this.username = username;}publicvoidsetAccountNonExpired(booleanaccountNonExpired){this.accountNonExpired = accountNonExpired;}publicvoidsetAccountNonLocked(booleanaccountNonLocked){this.accountNonLocked = accountNonLocked;}publicvoidsetCredentialsNonExpired(booleancredentialsNonExpired){this.credentialsNonExpired = credentialsNonExpired;}publicvoidsetEnabled(booleanenabled){this.enabled = enabled;}publicvoidsetAuthorities(Collection authorities){this.authorities = authorities;}}
添加JwtAuthService
JwtAuthService是认证服务的Service
/*** 认证登录服务*/publicclassJwtAuthService{privateAuthenticationManager authenticationManager;privateJwtTokenUtil jwtTokenUtil;@ResourceprivateMyUserDetailsServiceMapper myUserDetailsServiceMapper;privateJwtAuthService(){}publicJwtAuthService(AuthenticationManager authenticationManager,JwtTokenUtil jwtTokenUtil){this.authenticationManager = authenticationManager;this.jwtTokenUtil = jwtTokenUtil;}/*** 登录认证换取JWT令牌*@returnJWT*/publicStringlogin(String username,String password,Map payloads) throwsBaseException{try{UsernamePasswordAuthenticationToken upToken =newUsernamePasswordAuthenticationToken(username, password);Authentication authentication = authenticationManager.authenticate(upToken);SecurityContextHolder.getContext().setAuthentication(authentication);}catch(AuthenticationException e){thrownewBaseException(ResultCode.FAILURE.getCode(),"用户名或者密码输入错误,或者新建用户未赋予角色权限!");}returnjwtTokenUtil.generateToken(username,payloads);}publicStringrefreshToken(String oldToken){if(!jwtTokenUtil.isTokenExpired(oldToken)){returnjwtTokenUtil.refreshToken(oldToken);}returnnull;}/*** 获取角色信息列表*@paramtoken*@return*/publicListroles(String token) {String username = jwtTokenUtil.getUsernameFromToken(token);//加载用户角色列表List roleCodes =myUserDetailsServiceMapper.findRoleByUserName(username);returnroleCodes;}}
添加MyRBACService
MyRBACService用户角色service
/*** 权限服务*/@Component("verseService")publicclassMyRBACService{privateAntPathMatcher antPathMatcher =newAntPathMatcher();@ResourceprivateVerseApiProperties verseApiProperties;@ResourceprivateMyUserDetailsServiceMapper myUserDetailsServiceMapper;/*** 判断某用户是否具有该request资源的访问权限*/publicbooleanhasPermission(HttpServletRequest request, Authentication authentication){Object principal = authentication.getPrincipal();if(principalinstanceofUserDetails){UserDetails userDetails = ((UserDetails)principal);List authorityList =AuthorityUtils.commaSeparatedStringToAuthorityList(request.getRequestURI());returnuserDetails.getAuthorities().contains(authorityList.get(0))|| verseApiProperties.getIgnoreUrl().contains(request.getRequestURI());}returnfalse;}publicMyUserDetailsfindByUserName(String username){returnmyUserDetailsServiceMapper.findByUserName(username);}publicListfindRoleByUserName(String username) {returnmyUserDetailsServiceMapper.findRoleByUserName(username);}publicListfindApiByRoleCode(String roleCode) {returnmyUserDetailsServiceMapper.findApiByRoleCode(roleCode);}}
添加MyUserDetailsService
@ComponentpublicclassMyUserDetailsServiceimplementsUserDetailsService{@ResourceMyRBACService myRBACService;@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{//加载基础用户信息MyUserDetails myUserDetails = myRBACService.findByUserName(username);//加载用户角色列表List roleCodes = myRBACService.findRoleByUserName(username);List authorities =newArrayList<>();for(String roleCode : roleCodes){//通过用户角色列表加载用户的资源权限列表authorities.addAll(myRBACService.findApiByRoleCode(roleCode));}//角色是一个特殊的权限,ROLE_前缀roleCodes = roleCodes.stream().map(rc ->"ROLE_"+rc).collect(Collectors.toList());authorities.addAll(roleCodes);myUserDetails.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",",authorities)));returnmyUserDetails;}}
添加JwtAuthenticationTokenFilter
在用户名和密码校验前添加的过滤器,如果请求中有jwt的token且有效,会取出token中的用户名,然后调用SpringSecurity的API进行登录操作。
/*** JWT令牌授权过滤器* 1.判断令牌的有效性* 2.根据令牌为该用户授权可以访问的资源*/@Slf4j@Configurationpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter {private JwtProperties jwtProperties;private JwtTokenUtil jwtTokenUtil;private MyUserDetailsService myUserDetailsService;privateJwtAuthenticationTokenFilter(){}public JwtAuthenticationTokenFilter(JwtProperties jwtProperties,JwtTokenUtil jwtTokenUtil,MyUserDetailsService myUserDetailsService) {this.jwtProperties = jwtProperties;this.jwtTokenUtil = jwtTokenUtil;this.myUserDetailsService = myUserDetailsService;}@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain)throws ServletException, IOException {//获取tokenString authHeader = request.getHeader(jwtProperties.getHeader());if(!StrUtil.isEmpty(authHeader) ) {String username = jwtTokenUtil.getUsernameFromToken(authHeader);logger.info("checking username:"+ username);if(username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = this.myUserDetailsService.loadUserByUsername(username);if(jwtTokenUtil.validateToken(authHeader, username)) {//给使用该JWT令牌的用户进行授权UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());logger.info("authenticated user:"+username);SecurityContextHolder.getContext().setAuthentication(authentication);}}}filterChain.doFilter(request, response);}}
添加JwtAuthController
JwtAuthController主要用户获取token,刷新token
@RestController@Api(tags ="JwtAuthController")@RequestMapping("/jwtauth")publicclassJwtAuthController{@ResourceprivateJwtProperties jwtProperties;@ResourceprivateJwtAuthService jwtAuthService;@ResourceprivateMyUserDetailsServiceMapper myUserDetailsServiceMapper;/*** 使用用户名密码换JWT令牌*/@ApiOperation(value = JWTConstants.CONTROLLER_AUTHENTICATION)@RequestMapping(value = JWTConstants.CONTROLLER_AUTHENTICATION)publicResultlogin(@RequestBody Map map) {String username = map.get(jwtProperties.getUserParamName());String password = map.get(jwtProperties.getPwdParamName());if(StrUtil.isEmpty(username)|| StrUtil.isEmpty(password)){returnResult.fail(ResultCode.Unauthorized,"用户名或者密码不能为空");}try{returnResult.data(jwtAuthService.login(username, password,null));}catch(BaseException e){returnResult.fail(ResultCode.FAILURE,e.getMessage());}}/*** 刷新JWT令牌*/@ApiOperation(value = JWTConstants.CONTROLLER_REFRESH)@RequestMapping(value = JWTConstants.CONTROLLER_REFRESH)publicResultrefresh(@RequestHeader("${verse.jwt.header}")String token){returnResult.data(jwtAuthService.refreshToken(token));}/*** 获取用户角色列表接口*/@ApiOperation(value = JWTConstants.CONTROLLER_ROLES)@RequestMapping(value = JWTConstants.CONTROLLER_ROLES)publicResultroles(@RequestHeader("${verse.jwt.header}")String token){returnResult.data(jwtAuthService.roles(token));}}
修改Swagger的配置
通过修改配置实现调用接口自带Authorization头,这样就可以访问需要登录的接口了。
packagecom.verse.jwt.config;importio.swagger.annotations.Api;importorg.springframework.beans.BeansException;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.beans.factory.config.BeanPostProcessor;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.ReflectionUtils;importorg.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;importspringfox.documentation.builders.ApiInfoBuilder;importspringfox.documentation.builders.PathSelectors;importspringfox.documentation.builders.RequestHandlerSelectors;importspringfox.documentation.service.*;importspringfox.documentation.spi.DocumentationType;importspringfox.documentation.spi.service.contexts.SecurityContext;importspringfox.documentation.spring.web.plugins.Docket;importspringfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;importspringfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;importjava.lang.reflect.Field;importjava.util.ArrayList;importjava.util.List;importjava.util.stream.Collectors;@ConfigurationpublicclassSwagger2Config{@Value("${verse.jwt.header}")privateString header ;@BeanpublicDocketcreateRestApi(){returnnewDocket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()//.apis(RequestHandlerSelectors.basePackage("com.verse.jwt.system.controller"))//为有@Api注解的Controller生成API文档.apis(RequestHandlerSelectors.withClassAnnotation(Api.class)).paths(PathSelectors.any()).build()//添加登录认证.securitySchemes(securitySchemes()).securityContexts(securityContexts());}privateListsecuritySchemes() {//设置请求头信息List result =newArrayList<>();ApiKey apiKey =newApiKey("JWTVerseHeaderName","JWTVerseHeaderName","header");result.add(apiKey);returnresult;}privateListsecurityContexts() {//设置需要登录认证的路径List result =newArrayList<>();result.add(getContextByPath("/sys-role/.*"));returnresult;}privateSecurityContextgetContextByPath(String pathRegex){returnSecurityContext.builder().securityReferences(defaultAuth()).forPaths(PathSelectors.regex(pathRegex)).build();}privateListdefaultAuth() {List result =newArrayList<>();AuthorizationScope authorizationScope =newAuthorizationScope("global","accessEverything");AuthorizationScope[] authorizationScopes =newAuthorizationScope[1];authorizationScopes[0] = authorizationScope;result.add(newSecurityReference("JWTVerseHeaderName", authorizationScopes));returnresult;}privateApiInfoapiInfo(){returnnewApiInfoBuilder().title("SwaggerUI演示").description("verse").contact(newContact("springboot葵花宝典",null,null)).version("1.0").build();}@BeanpublicstaticBeanPostProcessorspringfoxHandlerProviderBeanPostProcessor(){returnnewBeanPostProcessor() {@OverridepublicObjectpostProcessAfterInitialization(Object bean, String beanName)throwsBeansException{if(beaninstanceofWebMvcRequestHandlerProvider || beaninstanceofWebFluxRequestHandlerProvider) {customizeSpringfoxHandlerMappings(getHandlerMappings(bean));}returnbean;}privatevoidcustomizeSpringfoxHandlerMappings(List mappings) {List copy = mappings.stream().filter(mapping -> mapping.getPatternParser() ==null).collect(Collectors.toList());mappings.clear();mappings.addAll(copy);}@SuppressWarnings("unchecked")privateListgetHandlerMappings(Object bean) {try{Field field = ReflectionUtils.findField(bean.getClass(),"handlerMappings");field.setAccessible(true);return(List) field.get(bean);}catch(IllegalArgumentException | IllegalAccessException e) {thrownewIllegalStateException(e);}}};}}
认证与授权流程演示
运行项目,访问API
Swagger api地址:http://localhost:8888/swagger-ui/
免密登录访问接口
未登录前访问接口
获取token
点击Authorize按钮,在弹框中输入登录接口中获取到的token信息
登录后访问获取权限列表接口,发现已经可以正常访问
如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!
原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!
声明:本文部分素材转载自互联网,如有侵权立即删除 。
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!
8. 精力有限,不少源码未能详细测试(解密),不能分辨部分源码是病毒还是误报,所以没有进行任何修改,大家使用前请进行甄别
丞旭猿论坛
暂无评论内容