Create New Project
src
pom.xml
中添加依赖管理
4.0.0
com.verse
verse
1.0.0
前后端分离verse
1.8
UTF-8
UTF-8
1.8
1.8
2.5.13
3.5.1
5.5.7
3.0.0
org.springframework.boot
spring-boot-starter-parent
${spring.parent}
pom
import
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus-version}
cn.hutool
hutool-all
${hutool.all.version}
io.springfox
springfox-boot-starter
${swagger.version}
org.apache.maven.plugins
maven-compiler-plugin
${maven.compiler.target}
通用异常处理以及通用响应数据结构等内容
com.verse.commons
、ArtiactID:verse-commons
点击 Finishverse-commons
的pom.xml
verse
com.verse
1.0.0
4.0.0
com.verse.commons
verse-commons
org.projectlombok
lombok
true
provided
com.fasterxml.jackson.core
jackson-annotations
org.springframework
spring-web
provided
/**
* 返回码接口
*/
public interface IResultCode {
/**
* 返回码
*
* @return int
*/
int getCode();
/**
* 返回消息
*
* @return String
*/
String getMsg();
}
package com.verse.commons.api;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 返回码实现
*/
@Getter
@AllArgsConstructor
public enum ResultCode 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");
/**
* 状态码
*/
final int code;
/**
* 消息内容
*/
final String msg;
}
/**
* 统一响应消息报文
* @param
*/
@Data
@Getter
public class Result implements Serializable {
private static final long serialVersionUID = 1L;
private int code;
private String msg;
private long time;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;
private Result() {
this.time = System.currentTimeMillis();
}
private Result(IResultCode resultCode) {
this(resultCode, null, resultCode.getMsg());
}
private Result(IResultCode resultCode, String msg) {
this(resultCode, null, msg);
}
private Result(IResultCode resultCode, T data) {
this(resultCode, data, resultCode.getMsg());
}
private Result(IResultCode resultCode, T data, String msg) {
this(resultCode.getCode(), data, msg);
}
private Result(int code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
this.time = System.currentTimeMillis();
}
/**
* 返回状态码
*
* @param resultCode 状态码
* @param 泛型标识
* @return ApiResult
*/
public static Result success(IResultCode resultCode) {
return new Result<>(resultCode);
}
public static Result success(String msg) {
return new Result<>(ResultCode.SUCCESS, msg);
}
public static Result success(IResultCode resultCode, String msg) {
return new Result<>(resultCode, msg);
}
public static Result data(T data) {
return data(data, VerseConstant.DEFAULT_SUCCESS_MESSAGE);
}
public static Result data(T data, String msg) {
return data(ResultCode.SUCCESS.code, data, msg);
}
public static Result data(int code, T data, String msg) {
return new Result<>(code, data, data == null ? VerseConstant.DEFAULT_NULL_MESSAGE : msg);
}
public static Result fail() {
return new Result<>(ResultCode.FAILURE, ResultCode.FAILURE.getMsg());
}
public static Result fail(String msg) {
return new Result<>(ResultCode.FAILURE, msg);
}
public static Result fail(int code, String msg) {
return new Result<>(code, null, msg);
}
public static Result fail(IResultCode resultCode) {
return new Result<>(resultCode);
}
public static Result fail(IResultCode resultCode, String msg) {
return new Result<>(resultCode, msg);
}
public static Result condition(boolean flag) {
return flag ? success(VerseConstant.DEFAULT_SUCCESS_MESSAGE) : fail(VerseConstant.DEFAULT_FAIL_MESSAGE);
}
}
package com.verse.commons.exception;
import com.verse.commons.api.ResultCode;
import lombok.Data;
import org.springframework.http.HttpStatus;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
/**
* 基础异常处理类
*/
@Data
public class BaseException extends RuntimeException{
private static final long serialVersionUID = 5782968730281544562L;
private int status = INTERNAL_SERVER_ERROR.value();
public BaseException(String message) {
super(message);
}
public BaseException(HttpStatus status, String message) {
super(message);
this.status = status.value();
}
public BaseException(int code, String message) {
super(message);
this.code = code;
this.message =message;
}
//异常错误编码
private int code ;
//异常信息
private String message;
private BaseException(){}
public BaseException(ResultCode resultCode) {
this.code = resultCode.getCode();
this.message = resultCode.getMsg();
}
}
/**
* verse基本常量
*/
public class VerseConstant {
/**
* 默认成功消息
*/
public static final String DEFAULT_SUCCESS_MESSAGE = "处理成功";
/**
* 默认失败消息
*/
public static final String DEFAULT_FAIL_MESSAGE = "处理失败";
/**
* 默认为空消息
*/
public static final String DEFAULT_NULL_MESSAGE = "承载数据为空";
}
项目的后端核心服务(Spring Boot web应用)
com.verse.jwt
、ArtiactID:verse-jwt
点击 Finish
verse
com.verse
1.0.0
4.0.0
com.verse.jwt
verse-jwt
org.springframework.boot
spring-boot-starter-web
com.verse.commons
verse-commons
1.0.0
mysql
mysql-connector-java
runtime
com.baomidou
mybatis-plus-boot-starter
com.baomidou
mybatis-plus-generator
org.apache.velocity
velocity
io.springfox
springfox-boot-starter
@MapperScan("com.verse.jwt.*.mapper")
@SpringBootApplication
public class VerseJwtApplication {
public static void main(String[] args) {
SpringApplication.run(VerseJwtApplication.class, args);
}
}
参考mybatis-plus
代码生成:https://baomidou.com/pages/779a6e/
com.baomidou
mybatis-plus-generator
DATA_SOURCE_CONFIG:数据链接配置
generator:根据模板生成代码
getTables方法:数据库表,all表示库中所有表
将模板添加到src\main\resources\templates
文件下
public class MybatisPlusGenerator {
private static final DataSourceConfig.Builder DATA_SOURCE_CONFIG = new DataSourceConfig
.Builder("jdbc:mysql://localhost:3306/verse?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai", "root", "root");
public static void generator(){
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 情况
protected static List getTables(String tables) {
return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
}
}
public class GeneratorMain {
public static void main(String[] args) {
MybatisPlusGenerator.generator();
}
}
生成结果如下
将system包放在verse-jwt
的com.verse.jwt包下,结果如下:
将mapper放在verse-jwt
的src\main\resources
包下,结果如下:
5.整合Swagger-ui实现在线API文档
在
verse-jwt
项目的pom.xml中新增Swagger-UI相关依赖
io.springfox
springfox-boot-starter
在verse-jwt
项目中添加如下类Swagger2Config
:
package com.verse.jwt.config;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.verse.jwt.system.controller"))
.paths(PathSelectors.any())
.build()
;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("SwaggerUI演示")
.description("verse")
.contact(new Contact("springboot葵花宝典", null, null))
.version("1.0")
.build();
}
@Bean
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return bean;
}
private void customizeSpringfoxHandlerMappings(List mappings) {
List copy = mappings.stream()
.filter(mapping -> mapping.getPatternParser() == null)
.collect(Collectors.toList());
mappings.clear();
mappings.addAll(copy);
}
@SuppressWarnings("unchecked")
private List getHandlerMappings(Object bean) {
try {
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
field.setAccessible(true);
return (List) field.get(bean);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
};
}
}
修改application.yml
文件,MVC默认的路径匹配策略为PATH_PATTERN_PARSER
,需要修改为ANT_PATH_MATCHER
;
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
url: jdbc:mysql://localhost:3306/verse?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
config-location:
mapper-locations:
- classpath:mapper/*.xml
- classpath*:com/**/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
springfox:
documentation:
enabled: true
server:
port: 8888
import com.verse.jwt.system.entity.SysUser;
import com.baomidou.mybatisplus.extension.service.IService;
/**
*
* 用户信息表 服务类
*
*
* @author springboot葵花宝典
* @since 2022-04-27
*/
public interface ISysUserService extends IService {
SysUser getUserByUserName(String userName);
}
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.verse.jwt.system.entity.SysUser;
import com.verse.jwt.system.mapper.SysUserMapper;
import com.verse.jwt.system.service.ISysUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import javax.annotation.Resource;
/**
*
* 用户信息表 服务实现类
*
*
* @author springboot葵花宝典
* @since 2022-04-27
*/
@Service
public class SysUserServiceImpl extends ServiceImpl implements ISysUserService {
@Resource
private SysUserMapper sysUserMapper;
/**
* 根据登录用户名查询用户信息
* @param userName 用户信息
* @return
*/
@Override
public SysUser getUserByUserName(String userName){
Assert.isTrue(StrUtil.isNotEmpty(userName),
"查询参数用户名不存在");
SysUser sysUser = sysUserMapper.selectOne(
new QueryWrapper().eq("username",userName));
if(sysUser != null){
sysUser.setPassword(""); //清空密码信息
}
return sysUser;
}
}
import com.verse.jwt.system.entity.SysUser;
import com.verse.jwt.system.service.ISysUserService;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
*
* 用户信息表 前端控制器
*
*
* @author springboot葵花宝典
* @since 2022-04-27
*/
@RestController
@Api(tags = "SysUserController", description =" 用户信息表")
@RequestMapping("/sys-user")
public class SysUserController {
@Resource
private ISysUserService sysuserService;
/**
* 根据登录用户名查询用户信息
* @param username 用户名称
* @return
*/
@ApiOperation(value = "info")
@GetMapping(value = "/info")
public SysUser info(@RequestParam("username") String username) {
return sysuserService.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
项目依赖
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt
/**
* jwt配置的属性
*/
@Data
@Component
@ConfigurationProperties("verse.jwt")
public class JwtProperties {
//是否开启JWT,即注入相关的类对象
private Boolean enabled;
//JWT密钥
private String secret;
//JWT有效时间
private Long expiration;
//前端向后端传递JWT时使用HTTP的header名称
private String header;
//用户获取JWT令牌发送的用户名参数名称
private String userParamName = "username";
//用户获取JWT令牌发送的密码参数名称
private String pwdParamName = "password";
//允许哪些域对本服务的跨域请求
private List corsAllowedOrigins;
//允许哪些HTTP方法跨域
private List corsAllowedMethods;
//是否关闭csrf跨站攻击防御功能
private Boolean csrfDisabled = true;
//是否使用默认的JWTAuthController
private Boolean useDefaultController = true;
}
VerseApiProperties权限全面开放的接口,不需要JWT令牌就可以访问,或者开发过程临时开放的URI
/**
* 权限全面开放的接口,不需要JWT令牌就可以访问,或者开发过程临时开放的URI
*/
@Data
@Component
@ConfigurationProperties(prefix = "verse.uaa")
public class VerseApiProperties {
/**
* 监控中心和swagger需要访问的url
*/
public static final String[] ENDPOINTS = {
"/jwtauth/**",
"/swagger-ui/swagger-resources/**",
"/swagger-resources/**",
"/webjars/**",
"/swagger-ui/**",
"/v2/api-docs",
"/v3/api-docs",
};
/**
* 忽略URL,List列表形式
*/
private List ignoreUrl = new ArrayList<>();
/**
* 首次加载合并ENDPOINTS
*/
@PostConstruct
public void initIgnoreUrl() {
Collections.addAll(ignoreUrl, ENDPOINTS);
}
}
application.yml
添加注解verse:
jwt:
enabled: true
secret: verse
expiration: 3600000
header: JWTHeaderName
userParamName: username
pwdParamName: password
corsAllowedOrigins:
- http://localhost:8080
- http://127.0.0.1:8080
corsAllowedMethods:
- GET
- POST
useDefaultController: true
uaa:
ignoreUrl: #权限全面开放的接口,不需要JWT令牌就可以访问,或者开发过程临时开放的URI
- /sys-user/info #根据用户名获取用户信息
相关方法说明:
String generateToken(String username,Map
String getUsernameFromToken(String token):从token中获取登录用户的信息
Boolean validateToken(String token, String usernameParam):判断token是否还有效
/**
* JwtToken生成工具类
*/
@Slf4j
@Component
public class JwtTokenUtil {
@Autowired
private JwtProperties jwtProperties;
private static final String CLAIM_KEY_CREATED = "created";
/**
* 根据用户信息生成JWT的token令牌
*
* @param username 用户
* @param payloads 令牌中携带的附加信息
* @return 令token牌
*/
public String generateToken(String username,
Map payloads) {
int payloadSizes = payloads == null? 0 : payloads.size();
Map claims = new HashMap<>(payloadSizes + 2);
claims.put("sub", username);
claims.put("created", new Date());
if(payloadSizes > 0){
for(Map.Entry entry:payloads.entrySet()){
claims.put(entry.getKey(),entry.getValue());
}
}
return generateToken(claims);
}
/**
* 从token中获取JWT中的负载
*
* @param token 令牌
* @return 数据声明
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(jwtProperties.getSecret())
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + + jwtProperties.getExpiration());
}
/**
* 从token令牌中获取登录用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token令牌
* @param usernameParam 用户名的唯一标识
* @return 是否有效
*/
public Boolean validateToken(String token, String usernameParam) {
//根据toekn获取用户名
String username = getUsernameFromToken(token);
return (username.equals(usernameParam) && !isTokenExpired(token));
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) {
try {
//根据token获取获取过期时间
Date expiration = getExpiredDateFromToken( token);
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据负责生成JWT的token
*
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map claims) {
//生成token的过期时间
Date expirationDate = generateExpirationDate();
return Jwts.builder().setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
.compact();
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
//获取负载信息
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 从token令牌中获取登录用户名
*
* @return 用户名
*/
public String getUsernameFromToken() {
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;
}
return username;
}
}
/**
* Spring Security 配置
* 可以配置多个WebSecurityConfigurerAdapter
* 但是多个Adaptor有执行顺序,默认值是100
* 这里设置为1会优先执行
*/
@Configuration
@Order(1)
public class JwtSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Resource
private JwtProperties jwtProperties;
@Resource
private VerseApiProperties apiProperties;
@Resource
private MyUserDetailsService myUserDetailsService;
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Resource
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Override
public void configure(HttpSecurity http) throws Exception {
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);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) {
//将项目中静态资源路径开放出来
web.ignoring().antMatchers(apiProperties.ENDPOINTS);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 跨站资源共享配置
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(jwtProperties.getCorsAllowedOrigins());
configuration.setAllowedMethods(jwtProperties.getCorsAllowedMethods());
configuration.applyPermitDefaultValues();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Override
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public JwtAuthService jwtAuthService(JwtTokenUtil jwtTokenUtil) throws Exception {
return new JwtAuthService(
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
import cn.hutool.json.JSONUtil;
import com.verse.commons.api.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 当访问接口没有权限时,自定义的返回结果
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(Result.fail(e.getMessage())));
response.getWriter().flush();
}
}
import cn.hutool.json.JSONUtil;
import com.verse.commons.api.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 当未登录或者token失效访问接口时,自定义的返回结果
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(Result.fail(authException.getMessage())));
response.getWriter().flush();
}
}
import com.verse.jwt.auth.dto.MyUserDetails;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 用户信息Mapper
*/
public interface MyUserDetailsServiceMapper {
//根据username查询用户信息
@Select("SELECT username,password,enabled\n" +
"FROM sys_user u\n" +
"WHERE u.username = #{username}")
MyUserDetails findByUserName(@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}")
List findRoleByUserName(@Param("username") String username);
//根据用户角色查询用户菜单权限
@Select({
""
})
List findMenuByRoleCodes(@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"
)
List findApiByRoleCode(@Param("roleCode") String roleCode);
}
public class MyUserDetails implements UserDetails {
String password; //密码
String username; //用户名
boolean accountNonExpired; //是否没过期
boolean accountNonLocked; //是否没被锁定
boolean credentialsNonExpired; //是否没过期
boolean enabled; //账号是否可用
Collection extends GrantedAuthority> authorities; //用户的权限集合
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setAuthorities(Collection extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
}
JwtAuthService是认证服务的Service
/**
* 认证登录服务
*/
public class JwtAuthService {
private AuthenticationManager authenticationManager;
private JwtTokenUtil jwtTokenUtil;
@Resource
private MyUserDetailsServiceMapper myUserDetailsServiceMapper;
private JwtAuthService(){}
public JwtAuthService(AuthenticationManager authenticationManager,
JwtTokenUtil jwtTokenUtil) {
this.authenticationManager = authenticationManager;
this.jwtTokenUtil = jwtTokenUtil;
}
/**
* 登录认证换取JWT令牌
* @return JWT
*/
public String login(String username,
String password,
Map payloads) throws BaseException {
try {
UsernamePasswordAuthenticationToken upToken =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}catch (AuthenticationException e){
throw new BaseException(ResultCode.FAILURE.getCode()
,"用户名或者密码输入错误,或者新建用户未赋予角色权限!");
}
return jwtTokenUtil.generateToken(username,payloads);
}
public String refreshToken(String oldToken){
if(!jwtTokenUtil.isTokenExpired(oldToken)){
return jwtTokenUtil.refreshToken(oldToken);
}
return null;
}
/**
* 获取角色信息列表
* @param token
* @return
*/
public List roles(String token){
String username = jwtTokenUtil.getUsernameFromToken(token);
//加载用户角色列表
List roleCodes =
myUserDetailsServiceMapper.findRoleByUserName(username);
return roleCodes;
}
}
MyRBACService用户角色service
/**
* 权限服务
*/
@Component("verseService")
public class MyRBACService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Resource
private VerseApiProperties verseApiProperties;
@Resource
private MyUserDetailsServiceMapper myUserDetailsServiceMapper;
/**
* 判断某用户是否具有该request资源的访问权限
*/
public boolean hasPermission(HttpServletRequest request, Authentication authentication){
Object principal = authentication.getPrincipal();
if(principal instanceof UserDetails){
UserDetails userDetails = ((UserDetails)principal);
List authorityList =
AuthorityUtils.commaSeparatedStringToAuthorityList(request.getRequestURI());
return userDetails.getAuthorities().contains(authorityList.get(0))
|| verseApiProperties.getIgnoreUrl().contains(request.getRequestURI());
}
return false;
}
public MyUserDetails findByUserName(String username) {
return myUserDetailsServiceMapper.findByUserName(username);
}
public List findRoleByUserName(String username) {
return myUserDetailsServiceMapper.findRoleByUserName(username);
}
public List findApiByRoleCode(String roleCode) {
return myUserDetailsServiceMapper.findApiByRoleCode(roleCode);
}
}
@Component
public class MyUserDetailsService implements UserDetailsService {
@Resource
MyRBACService myRBACService;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//加载基础用户信息
MyUserDetails myUserDetails = myRBACService.findByUserName(username);
//加载用户角色列表
List roleCodes = myRBACService.findRoleByUserName(username);
List authorities = new ArrayList<>();
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)
)
);
return myUserDetails;
}
}
在用户名和密码校验前添加的过滤器,如果请求中有jwt的token且有效,会取出token中的用户名,然后调用SpringSecurity的API进行登录操作。
/**
* JWT令牌授权过滤器
* 1.判断令牌的有效性
* 2.根据令牌为该用户授权可以访问的资源
*/
@Slf4j
@Configuration
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private JwtProperties jwtProperties;
private JwtTokenUtil jwtTokenUtil;
private MyUserDetailsService myUserDetailsService;
private JwtAuthenticationTokenFilter(){}
public JwtAuthenticationTokenFilter(JwtProperties jwtProperties,
JwtTokenUtil jwtTokenUtil,
MyUserDetailsService myUserDetailsService) {
this.jwtProperties = jwtProperties;
this.jwtTokenUtil = jwtTokenUtil;
this.myUserDetailsService = myUserDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
//获取token
String 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主要用户获取token,刷新token
@RestController
@Api(tags = "JwtAuthController")
@RequestMapping("/jwtauth")
public class JwtAuthController {
@Resource
private JwtProperties jwtProperties;
@Resource
private JwtAuthService jwtAuthService;
@Resource
private MyUserDetailsServiceMapper myUserDetailsServiceMapper;
/**
* 使用用户名密码换JWT令牌
*/
@ApiOperation(value = JWTConstants.CONTROLLER_AUTHENTICATION)
@RequestMapping(value = JWTConstants.CONTROLLER_AUTHENTICATION)
public Result login(@RequestBody Map map){
String username = map.get(jwtProperties.getUserParamName());
String password = map.get(jwtProperties.getPwdParamName());
if(StrUtil.isEmpty(username)
|| StrUtil.isEmpty(password)){
return Result.fail(
ResultCode.Unauthorized,
"用户名或者密码不能为空");
}
try {
return Result.data(
jwtAuthService.login(username, password,null));
}catch (BaseException e){
return Result.fail(ResultCode.FAILURE,e.getMessage());
}
}
/**
* 刷新JWT令牌
*/
@ApiOperation(value = JWTConstants.CONTROLLER_REFRESH)
@RequestMapping(value = JWTConstants.CONTROLLER_REFRESH)
public Result refresh(@RequestHeader("${verse.jwt.header}") String token){
return Result.data(jwtAuthService.refreshToken(token));
}
/**
* 获取用户角色列表接口
*/
@ApiOperation(value = JWTConstants.CONTROLLER_ROLES)
@RequestMapping(value = JWTConstants.CONTROLLER_ROLES)
public Result roles(
@RequestHeader("${verse.jwt.header}") String token){
return Result.data(jwtAuthService.roles(token));
}
}
通过修改配置实现调用接口自带Authorization头,这样就可以访问需要登录的接口了。
package com.verse.jwt.config;
import io.swagger.annotations.Api;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
public class Swagger2Config {
@Value("${verse.jwt.header}")
private String header ;
@Bean
public Docket createRestApi() {
return new Docket(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())
;
}
private List securitySchemes() {
//设置请求头信息
List result = new ArrayList<>();
ApiKey apiKey = new ApiKey("JWTVerseHeaderName", "JWTVerseHeaderName", "header");
result.add(apiKey);
return result;
}
private List securityContexts() {
//设置需要登录认证的路径
List result = new ArrayList<>();
result.add(getContextByPath("/sys-role/.*"));
return result;
}
private SecurityContext getContextByPath(String pathRegex) {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex))
.build();
}
private List defaultAuth() {
List result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
result.add(new SecurityReference("JWTVerseHeaderName", authorizationScopes));
return result;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("SwaggerUI演示")
.description("verse")
.contact(new Contact("springboot葵花宝典", null, null))
.version("1.0")
.build();
}
@Bean
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return bean;
}
private void customizeSpringfoxHandlerMappings(List mappings) {
List copy = mappings.stream()
.filter(mapping -> mapping.getPatternParser() == null)
.collect(Collectors.toList());
mappings.clear();
mappings.addAll(copy);
}
@SuppressWarnings("unchecked")
private List getHandlerMappings(Object bean) {
try {
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
field.setAccessible(true);
return (List) field.get(bean);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
};
}
}
Swagger api地址:http://localhost:8888/swagger-ui/
如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!
springboot葵花宝典
主要分享JAVA技术,主要包含SpringBoot、SpingCloud、Docker、中间件等技术,以及Github开源项目
93篇原创内容
公众号
原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!