springboot security+redis+jwt+验证码 登录验证

您所在的位置:网站首页 redis做验证码 springboot security+redis+jwt+验证码 登录验证

springboot security+redis+jwt+验证码 登录验证

2023-08-25 14:42| 来源: 网络整理| 查看: 265

概述

  基于jwt的token认证方案

 验证码

  框架的搭建,可以自己根据网上搭建,或者看我博客springboot相关的博客,这边就不做介绍了。验证码生成可以利用Java第三方组件,引入

com.github.penggle kaptcha 2.3.2

配置验证码相关的属性

@Component public class KaptchaConfig { @Bean public DefaultKaptcha getDefaultKaptcha() { DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); Properties properties = new Properties(); /*是否使用边框*/ properties.setProperty("kaptcha.border","no"); /*验证码 边框颜色*/ //properties.setProperty("kaptcha.border.color","black"); /*验证码干扰线 颜色*/ properties.setProperty("kaptcha.noise.color","black"); /*验证码宽度*/ properties.setProperty("kaptcha.image.width","110"); /*验证码高度*/ properties.setProperty("kaptcha.image.height","40"); //properties.setProperty("kaptcha.session.key","code"); /*验证码颜色*/ properties.setProperty("kaptcha.textproducer.font.color","204,128,255"); /*验证码大小*/ properties.setProperty("kaptcha.textproducer.font.size","30"); properties.setProperty("kaptcha.textproducer.char.space","3"); /*验证码字数*/ properties.setProperty("kaptcha.textproducer.char.length","4"); /*验证码 背景渐变色 开始*/ properties.setProperty("kaptcha.background.clear.from","240,240,240"); /*验证码渐变色 结束*/ properties.setProperty("kaptcha.background.clear.to","240,240,240"); /*验证码字体*/ properties.setProperty("kaptcha.textproducer.font.names", "Arial,微软雅黑"); Config config = new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } }

配置相应的配置接口就能生成验证码,但是这钟样式有点不好看,如果自定义还非常麻烦,索性

 

 利用网上大佬写好的工具类(链接不见了,找到在加上)

import javax.imageio.ImageIO; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.LinearGradientPaint; import java.awt.Paint; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Random; /** * * Description:验证码工具类 * @author huangweicheng * @date 2019/10/23 */ public class VerifyCodeUtils { //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符 public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; private static Random random = new Random(); /** * 使用系统默认字符源生成验证码 * @param verifySize 验证码长度 * @return */ public static String generateVerifyCode(int verifySize){ return generateVerifyCode(verifySize, VERIFY_CODES); } /** * 使用指定源生成验证码 * @param verifySize 验证码长度 * @param sources 验证码字符源 * @return */ public static String generateVerifyCode(int verifySize, String sources){ if(sources == null || sources.length() == 0){ sources = VERIFY_CODES; } int codesLen = sources.length(); Random rand = new Random(System.currentTimeMillis()); StringBuilder verifyCode = new StringBuilder(verifySize); for(int i = 0; i < verifySize; i++){ verifyCode.append(sources.charAt(rand.nextInt(codesLen-1))); } return verifyCode.toString(); } /** * 生成随机验证码文件,并返回验证码值 * @param w * @param h * @param outputFile * @param verifySize * @return * @throws IOException */ public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException{ String verifyCode = generateVerifyCode(verifySize); outputImage(w, h, outputFile, verifyCode); return verifyCode; } /** * 输出随机验证码图片流,并返回验证码值 * @param w * @param h * @param os * @param verifySize * @return * @throws IOException */ public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException{ String verifyCode = generateVerifyCode(verifySize); outputImage(w, h, os, verifyCode); return verifyCode; } /** * 生成指定验证码图像文件 * @param w * @param h * @param outputFile * @param code * @throws IOException */ public static void outputImage(int w, int h, File outputFile, String code) throws IOException{ if(outputFile == null){ return; } File dir = outputFile.getParentFile(); if(!dir.exists()){ dir.mkdirs(); } try{ outputFile.createNewFile(); FileOutputStream fos = new FileOutputStream(outputFile); outputImage(w, h, fos, code); fos.close(); } catch(IOException e){ throw e; } } /** * 输出指定验证码图片流 * @param w * @param h * @param os * @param code * @throws IOException */ public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{ int verifySize = code.length(); BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); Random rand = new Random(); Graphics2D g2 = image.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); Color[] colors = new Color[5]; Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN, Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.YELLOW }; float[] fractions = new float[colors.length]; for(int i = 0; i < colors.length; i++){ colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)]; fractions[i] = rand.nextFloat(); } Arrays.sort(fractions); g2.setColor(Color.GRAY);// 设置边框色 g2.fillRect(0, 0, w, h); Color c = getRandColor(200, 250); g2.setColor(c);// 设置背景色 g2.fillRect(0, 2, w, h-4); //绘制干扰线 Random random = new Random(); g2.setColor(getRandColor(160, 200));// 设置线条的颜色 for (int i = 0; i < 20; i++) { int x = random.nextInt(w - 1); int y = random.nextInt(h - 1); int xl = random.nextInt(6) + 1; int yl = random.nextInt(12) + 1; g2.drawLine(x, y, x + xl + 40, y + yl + 20); } // 添加噪点 float yawpRate = 0.05f;// 噪声率 int area = (int) (yawpRate * w * h); for (int i = 0; i < area; i++) { int x = random.nextInt(w); int y = random.nextInt(h); int rgb = getRandomIntColor(); image.setRGB(x, y, rgb); } shear(g2, w, h, c);// 使图片扭曲 g2.setColor(getRandColor(100, 160)); int fontSize = h-4; Font font = new Font("Algerian", Font.ITALIC, fontSize); g2.setFont(font); char[] chars = code.toCharArray(); for(int i = 0; i < verifySize; i++){ AffineTransform affine = new AffineTransform(); affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2); g2.setTransform(affine); g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10); } g2.dispose(); ImageIO.write(image, "jpg", os); } private static Color getRandColor(int fc, int bc) { if (fc > 255) fc = 255; if (bc > 255) bc = 255; int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); } private static int getRandomIntColor() { int[] rgb = getRandomRgb(); int color = 0; for (int c : rgb) { color = color > 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); g.copyArea(0, i, w1, 1, (int) d, 0); if (borderGap) { g.setColor(color); g.drawLine((int) d, i, 0, i); g.drawLine((int) d + w1, i, w1, i); } } } private static void shearY(Graphics g, int w1, int h1, Color color) { int period = random.nextInt(40) + 10; // 50; boolean borderGap = true; int frames = 20; int phase = 7; for (int i = 0; i < w1; i++) { double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); g.copyArea(i, 0, 1, h1, 0, (int) d); if (borderGap) { g.setColor(color); g.drawLine(i, (int) d, i, 0); g.drawLine(i, (int) d + h1, i, h1); } } } public static void main(String[] args) throws IOException { String verifyCode = generateVerifyCode(4); System.out.println(verifyCode); } }

将生成的验证码放置到redis里,登录时候,从cookie取值,过滤器拦截验证(仅限PC端)

import com.google.code.kaptcha.impl.DefaultKaptcha;import io.swagger.annotations.ApiOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * * Description:用户相关接口 * @author huangweicheng * @date 2019/10/22 */ @RestController @RequestMapping("/user") public class UserController { private static final Logger log = LoggerFactory.getLogger(UserController.class); @Autowired private RedisTemplate redisTemplate; @RequestMapping("/verifyCode.jpg") @ApiOperation(value = "图片验证码") public void verifyCode(HttpServletRequest request, HttpServletResponse response) throws IOException { /*禁止缓存*/ response.setDateHeader("Expires",0); response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); response.addHeader("Cache-Control", "post-check=0, pre-check=0"); response.setHeader("Pragma", "no-cache"); response.setContentType("image/jpeg"); /*获取验证码*/ String code = VerifyCodeUtils.generateVerifyCode(4); /*验证码已key,value的形式缓存到redis 存放时间一分钟*/ log.info("验证码============>" + code); String uuid = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(uuid,code,1,TimeUnit.MINUTES); Cookie cookie = new Cookie("captcha",uuid); /*key写入cookie,验证时获取*/ response.addCookie(cookie); ServletOutputStream outputStream = response.getOutputStream(); //ImageIO.write(bufferedImage,"jpg",outputStream); VerifyCodeUtils.outputImage(110,40,outputStream,code); outputStream.flush(); outputStream.close(); } }

尝试访问接口,生成的验证码是不是比组件生成的验证码好看多了。

验证码过滤器

验证码生成后,哪些地方需要用到验证码,配置对应的路径,设置过滤器进行过滤,过滤器继承OncePerRequestFilter,这样能够确保在一次请求只通过一Filter,而不需要重复执行,对应的路径没有正确的验证码抛出一个自定义的异常进行统一处理。

import com.alibaba.fastjson.JSONObject;import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashSet; import java.util.Set; /** * * Description: 图片验证码过滤器 * @author huangweicheng * @date 2019/10/22 */ @Component public class ImageCodeFilter extends OncePerRequestFilter implements InitializingBean { /** * 哪些地址需要图片验证码进行验证 */ private Set urls = new HashSet(); private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Autowired private RedisTemplate redisTemplate; @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); urls.add("/hwc/user/login"); } @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { httpServletResponse.setContentType("application/json;"); boolean action = false; String t = httpServletRequest.getRequestURI(); for (String url : urls) { if (antPathMatcher.match(url,httpServletRequest.getRequestURI())) { action = true; break; } } if (action) { try { /*图片验证码是否正确*/ checkImageCode(httpServletRequest); }catch (ImageCodeException e){ JSONObject jsonObject = new JSONObject(); jsonObject.put("code", ResultModel.ERROR); jsonObject.put("msg",e.getMessage()); httpServletResponse.getWriter().write(jsonObject.toJSONString()); return; } } filterChain.doFilter(httpServletRequest,httpServletResponse); } /** * * Description:验证图片验证码是否正确 * @param httpServletRequest * @author huangweicheng * @date 2019/10/22 */ private void checkImageCode(HttpServletRequest httpServletRequest) { /*从cookie取值*/ Cookie[] cookies = httpServletRequest.getCookies(); String uuid = ""; for (Cookie cookie : cookies) { String cookieName = cookie.getName(); if ("captcha".equals(cookieName)) { uuid = cookie.getValue(); } } String redisImageCode = (String) redisTemplate.opsForValue().get(uuid); /*获取图片验证码与redis验证*/ String imageCode = httpServletRequest.getParameter("imageCode"); /*redis的验证码不能为空*/ if (StringUtils.isEmpty(redisImageCode) || StringUtils.isEmpty(imageCode)) { throw new ImageCodeException("验证码不能为空"); } /*校验验证码*/ if (!imageCode.equalsIgnoreCase(redisImageCode)) { throw new ImageCodeException("验证码错误"); } redisTemplate.delete(redisImageCode); } }

 自定义的验证码异常

import lombok.Data; import java.io.Serializable; /** * * Description:图片验证码相关异常 * @author huangweicheng * @date 2019/10/22 */ @Data public class ImageCodeException extends RuntimeException implements Serializable { private static final long serialVersionUID = 4554L; private String code; public ImageCodeException() { } public ImageCodeException(String message) { super(message); } public ImageCodeException(String code,String message) { super(message); this.code = code; } public ImageCodeException(String message,Throwable cause) { super(message,cause); } public ImageCodeException(Throwable cause) { super(cause); } public ImageCodeException(String message,Throwable cause,boolean enableSupperssion,boolean writablesStackTrace) { super(message,cause,enableSupperssion,writablesStackTrace); } }

过滤器统一处理

import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * * Description:全局变量捕获 * @author huangweicheng * @date 2019/10/22 */ @ControllerAdvice public class GlobalExceptionHandler { @ResponseBody @ExceptionHandler(Exception.class) public ResponseEntity exceptionHandler(Exception e) { e.printStackTrace(); ResultModel resultModel = new ResultModel(2,"系统出小差了,让网站管理员来处理吧 ಥ_ಥ"); return new ResponseEntity(resultModel, HttpStatus.OK); } @ResponseBody @ExceptionHandler(ImageCodeException.class) public ResponseEntity exceptionHandler(ImageCodeException e) { e.printStackTrace(); ResultModel resultModel = new ResultModel(2,e.getMessage()); return new ResponseEntity(resultModel,HttpStatus.OK); } }

说了这么多,只是我们token验证的开始

security

引入spring的security安全框架

     org.springframework.boot spring-boot-starter-security

最终的安全配置

import com.alibaba.fastjson.JSONObject;import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.security.NoSuchAlgorithmException; import java.security.Security; import java.util.concurrent.TimeUnit; /** * * Description:安全配置 * @author huangweicheng * @date 2019/10/21 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 日志记录 */ private static final Logger log = LoggerFactory.getLogger(Security.class); @Autowired private RedisTemplate redisTemplate; @Autowired protected SysUserDetailsServiceImpl sysUserDetailsService; @Autowired private ImageCodeFilter imageCodeFilter; @Autowired private JwtTokenUtil jwtTokenUtil; /** * * Description:资源角色配置登录 * @param http * @author huangweicheng * @date 2019/10/21 */ @Override protected void configure(HttpSecurity http) throws Exception { /*图片验证码过滤器设置在密码验证之前*/ http.addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers(HttpMethod.GET, "/", "/*.html", "favicon.ico", "/**/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() .antMatchers("/user/**","/login").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/hwc/**").hasRole("USER") .anyRequest().authenticated() .and().formLogin().loginProcessingUrl("/user/login") /*自定义登录成功处理,返回token值*/ .successHandler((HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication)-> { log.info("用户为====>" + httpServletRequest.getParameter("username") + "登录成功"); httpServletResponse.setContentType("application/json;charset=utf-8"); /*获取用户权限信息*/ UserDetails userDetails = (UserDetails) authentication.getPrincipal(); String token = jwtTokenUtil.generateToken(userDetails); /*存储redis并设置了过期时间*/ redisTemplate.boundValueOps(userDetails.getUsername() + "hwc").set(token,10, TimeUnit.MINUTES); JSONObject jsonObject = new JSONObject(); jsonObject.put("code", ResultModel.SUCCESS); jsonObject.put("msg","登录成功"); /*认证信息写入header*/ httpServletResponse.setHeader("Authorization",token); httpServletResponse.getWriter().write(jsonObject.toJSONString()); }) /*登录失败处理*/ .failureHandler((HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)-> { log.info("用户为====>" + request.getParameter("username") + "登录失败"); String content = exception.getMessage(); //TODO 后期改进密码错误方式,统一处理 String temp = "Bad credentials"; if (temp.equals(exception.getMessage())) { content = "用户名或密码错误"; } response.setContentType("application/json;charset=utf-8"); JSONObject jsonObject = new JSONObject(); jsonObject.put("code", ResultModel.ERROR); jsonObject.put("msg",content); jsonObject.put("content",exception.getMessage()); response.getWriter().write(jsonObject.toJSONString()); }) /*无权限访问处理*/ .and().exceptionHandling().accessDeniedHandler((HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e)-> { httpServletResponse.setContentType("application/json;charset=utf-8"); JSONObject jsonObject = new JSONObject(); jsonObject.put("code",HttpStatus.FORBIDDEN); jsonObject.put("msg", "无权限访问"); jsonObject.put("content",e.getMessage()); httpServletResponse.getWriter().write(jsonObject.toJSONString()); }) /*匿名用户访问无权限资源时的异常*/ .and().exceptionHandling().authenticationEntryPoint((HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)-> { response.setContentType("application/json;charset=utf-8"); JSONObject jsonObject = new JSONObject(); jsonObject.put("code",HttpStatus.FORBIDDEN); jsonObject.put("msg","无访问权限"); response.getWriter().write(jsonObject.toJSONString()); }) .and().authorizeRequests() /*基于token,所以不需要session*/ .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) /*由于使用的是jwt,这里不需要csrf防护并且禁用缓存*/ .and().csrf().disable().headers().cacheControl(); /*token过滤*/ http.addFilterBefore(authenticationTokenFilterBean(),UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(sysUserDetailsService).passwordEncoder(new PasswordEncoder() { /** * * Description:用户输入的密码加密 * @param charSequence * @author huangweicheng * @date 2019/10/21 */ @Override public String encode(CharSequence charSequence) { try { return Common.md5(charSequence.toString()); }catch (NoSuchAlgorithmException e){ e.printStackTrace(); } return null; } /** * * Description: 与数据库的密码匹配 * @param charSequence 用户密码 * @param encodedPassWord 数据库密码 * @author huangweicheng * @date 2019/10/21 */ @Override public boolean matches(CharSequence charSequence, String encodedPassWord) { try { return encodedPassWord.equals(Common.md5(charSequence.toString())); }catch (NoSuchAlgorithmException e){ e.printStackTrace(); } return false; } }); }   //token过滤器 @Bean public JwtAuthenticationFilter authenticationTokenFilterBean() { return new JwtAuthenticationFilter(); } }

注解很多都解释清楚,就不过多介绍了。因为security已经将实现登陆的功能封装完成,需要我们做的其实并不多,我们要做仅是查找用户,将查询用户的信息,包括密码,角色等等交给UserDtails,然后在配置里进行自定义验证(可以是md5或其他加密方式),持久层用的是jpa

用户类

import io.swagger.annotations.ApiModel; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * * Description:用户信息 * @author huangweicheng * @date 2019/10/21 */ @Entity @Data @ApiModel @Table(name = "t_sys_user") public class SysUserVo extends SysBaseVo implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private int id; @Column(name = "user_name") private String userName; @Column(name = "password") private String password; @Column(name = "error_num") private int errorNum; @Column(name = "password_weak") private int passwordWeak; @Column(name = "forbid") private int forbid; @Column(name = "uuid") private String uuid; /** * CascadeType.REMOVE 级联删除,FetchType.LAZY懒加载,不会马上从数据库中加载 * name中间表名称 * @JoinColumn t_sys_user的user_id与中间表user_id的映射关系 * @inverseJoinColumns 中间表另一字段与对应表关联关系 */ @ManyToMany(cascade = CascadeType.REMOVE,fetch = FetchType.EAGER) @JoinTable(name = "t_sys_user_roles",joinColumns = @JoinColumn(name="user_id",referencedColumnName = "user_id"),inverseJoinColumns = @JoinColumn(name = "role_id",referencedColumnName = "role_id")) private List roles; /** * * Description:权限信息 * @param * @author huangweicheng * @date 2019/10/21 */ @Override public Collection


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3