概述
基于jwt的token认证方案
验证码
框架的搭建,可以自己根据网上搭建,或者看我博客springboot相关的博客,这边就不做介绍了。验证码生成可以利用Java第三方组件,引入
<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
配置验证码相关的属性
@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 << 8; color = color | c; } return color; } private static int[] getRandomRgb() { int[] rgb = new int[3]; for (int i = 0; i < 3; i++) { rgb[i] = random.nextInt(255); } return rgb; } private static void shear(Graphics g, int w1, int h1, Color color) { shearX(g, w1, h1, color); shearY(g, w1, h1, color); } private static void shearX(Graphics g, int w1, int h1, Color color) { int period = random.nextInt(2); boolean borderGap = true; int frames = 1; int phase = random.nextInt(2); for (int i = 0; i < h1; i++) { double d = (double) (period >> 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<String> 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;charset=utf-8"); 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<ResultModel> exceptionHandler(Exception e) { e.printStackTrace(); ResultModel resultModel = new ResultModel(2,"系统出小差了,让网站管理员来处理吧 ಥ_ಥ"); return new ResponseEntity<>(resultModel, HttpStatus.OK); } @ResponseBody @ExceptionHandler(ImageCodeException.class) public ResponseEntity<ResultModel> exceptionHandler(ImageCodeException e) { e.printStackTrace(); ResultModel resultModel = new ResultModel(2,e.getMessage()); return new ResponseEntity<>(resultModel,HttpStatus.OK); } }
说了这么多,只是我们token验证的开始
security
引入spring的security安全框架
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
最终的安全配置
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<SysRoleVo> roles; /** * * Description:权限信息 * @param * @author huangweicheng * @date 2019/10/21 */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorityList = new ArrayList<>(); List<SysRoleVo> roles = this.getRoles(); for (SysRoleVo role : roles) { authorityList.add(new SimpleGrantedAuthority(role.getRoleName())); } return authorityList; } @Override public String getUsername() { return this.userName; } /** * * Description:账户是否过期 * @param * @author huangweicheng * @date 2019/10/21 */ @Override public boolean isAccountNonExpired() { return true; } /** * * Description:账户是否被冻结 * @param * @author huangweicheng * @date 2019/10/21 */ @Override public boolean isAccountNonLocked() { if (forbid != 1) { return false; } return true; } /** * * Description:账户密码是否过期,密码要求性高会使用到,比较每隔一段时间就要求用户重置密码 * @param * @author huangweicheng * @date 2019/10/21 */ @Override public boolean isCredentialsNonExpired() { return true; } /** * * Description:账户是否可用 * @param * @author huangweicheng * @date 2019/10/21 */ @Override public boolean isEnabled() { if (bUse != 1) { return false; } return true; } }
角色类Role
import io.swagger.annotations.ApiModel; import lombok.Data; import javax.persistence.*; @Entity @Data @ApiModel @Table(name = "t_sys_role") public class SysRoleVo extends SysBaseVo { @Id @GeneratedValue @Column(name = "role_id") private int roleId; @Column(name = "role_name") private String roleName; }
因为我喜欢把相同的属性抽出来,所以定义了一个基类,也可以不这么干
import io.swagger.annotations.ApiModel; import lombok.Data; import javax.persistence.*; @Entity @Data @ApiModel @Table(name = "t_sys_role") public class SysRoleVo extends SysBaseVo { @Id @GeneratedValue @Column(name = "role_id") private int roleId; @Column(name = "role_name") private String roleName; }
接下来就简单多了,只需要在定义一个实现类去实现UserDetailService,基本的登录其实就完成了。
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * * Description:账户详情信息 * @author huangweicheng * @date 2019/10/21 */ @Service public class SysUserDetailsServiceImpl implements UserDetailsService { @Resource private SysUserRepository sysUserRepository; @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { SysUserVo sysUser = sysUserRepository.findByUserName(userName); if (sysUser == null) { throw new UsernameNotFoundException(userName); } return sysUser; } }
JWT
jwt的相关介绍就不多废话了,不了解可以查看阮大神的博客
JwtTokenUtil工具类(剽窃林老师的代码)
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; /** * * Description: token相关的工具类 * @author huangweicheng * @date 2019/10/23 */ @Component public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -4324967L; private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; private static final String CLAIM_KEY_ROLES = "roles"; @Autowired private RedisTemplate redisTemplate; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; /** * * Description: 解析token,从token中获取信息 * @param token * @author huangweicheng * @date 2019/10/23 */ private Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }catch (Exception e){ e.printStackTrace(); claims = null; } return claims; } /** * * Description:获取用户名 * @param token * @author huangweicheng * @date 2019/10/23 */ public String getUserNameFromToken(String token) { String userName; try { final Claims claims = getClaimsFromToken(token); userName = claims.getSubject(); }catch (Exception e){ userName = null; } return userName; } /** * * Description:从token中获取 * @param token * @author huangweicheng * @date 2019/10/25 */ public String getRolesFromToken(String token) { String roles; try { final Claims claims = getClaimsFromToken(token); roles = (String) claims.get(CLAIM_KEY_ROLES); }catch (Exception e){ roles = null; } return roles; } /** * * Description:获取token创建时间 * @param token * @author huangweicheng * @date 2019/10/23 */ public Date getCreatedDateFromToken(String token) { Date created; try { final Claims claims = getClaimsFromToken(token); created = new Date((Long) claims.get(CLAIM_KEY_CREATED)); }catch (Exception e){ created = null; } return created; } /** * * Description: 获取token过期时间 * @param token * @author huangweicheng * @date 2019/10/23 */ public Date getExpirationDateFromToken(String token) { Date expiration; try { final Claims claims = getClaimsFromToken(token); expiration = claims.getExpiration(); }catch (Exception e){ expiration = null; } return expiration; } /** * * Description:token生成过期时间 * @param * @author huangweicheng * @date 2019/10/23 */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } /** * * Description:token是否过期 * @param token * @author huangweicheng * @date 2019/10/23 */ private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } /** * * Description:token创建时间与密码最后修改时间比较,小于返回true,大于返回false * @param created * @param lastPasswordReset * @author huangweicheng * @date 2019/10/24 */ private Boolean isCreatedBeforeLastPasswordReset(Date created,Date lastPasswordReset) { return (lastPasswordReset != null && created.before(lastPasswordReset)); } /** * * Description: 创建token * @param userDetails * @author huangweicheng * @date 2019/10/23 */ public String generateToken(UserDetails userDetails) { String roles = ""; Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); for (GrantedAuthority authority : authorities) { String temp = authority.getAuthority() + ","; roles += temp; } roles = roles.substring(0,roles.length() - 1); Map<String,Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED,new Date()); claims.put(CLAIM_KEY_ROLES,roles); return generateToken(claims); } /** * * Description:使用Rs256签名 * @param claims * @author huangweicheng * @date 2019/10/23 */ private String generateToken(Map<String,Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512,secret) .compact(); } /** * * Description:是否刷新token * @param token * @param lastPasswordReset * @author huangweicheng * @date 2019/10/23 */ public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) { final Date created = getCreatedDateFromToken(token); return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset) && !isTokenExpired(token); } /** * * Description:刷新token * @param token * @author huangweicheng * @date 2019/10/23 */ public String refreshToken(String token) { String refreshToken; try { final Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED,new Date()); refreshToken = generateToken(claims); }catch (Exception e){ refreshToken = null; } return refreshToken; } /** * * Description:验证token * @param token * @param userDetails * @author huangweicheng * @date 2019/10/24 */ public boolean validateToken(String token) { final String username = getUserNameFromToken(token); if (redisTemplate.hasKey(username + "huangweicheng") && !isTokenExpired(token)) { //如果验证成功,说明此用户进行了一次有效操作,延长token的过期时间 redisTemplate.boundValueOps(username + "subjectrace").expire(this.expiration,TimeUnit.MINUTES); return true; } return false; }
现在我们设置token过滤,请求接口没有token或者token已经过期,就会跳到登录页面
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * * Description:token的拦截器 * @author huangweicheng * @date 2019/10/24 */ @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Value("${jwt.header}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private RedisTemplate redisTemplate; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String token = httpServletRequest.getHeader(this.tokenHeader); if (token != null && jwtTokenUtil.validateToken(token)) { String role = jwtTokenUtil.getRolesFromToken(token); String[] roles = role.split(","); List<GrantedAuthority> authorityList = new ArrayList<>(); for (String r : roles) { authorityList.add(new SimpleGrantedAuthority(r)); } String username = jwtTokenUtil.getUserNameFromToken(token); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,null,authorityList); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); /*权限设置*/ SecurityContextHolder.getContext().setAuthentication(authenticationToken); } filterChain.doFilter(httpServletRequest,httpServletResponse); } }
现在验证的核心内容都已经完成,写几个接口测试下。
HomeController类
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class HomeController { @RequestMapping("/admin/test2") @ResponseBody public String admin2() { return "ROLE_ADMIN"; } }
HwcController类
@Controller public class HwcController { @GetMapping("/hwc/test") @ResponseBody public String test() { return "ROLE_USER"; } }
用postman测试一下,没token匿名访问
获取验证码后,将验证码写入cookie里,输入账号密码,登录
登录成功
token在放在header里
有token,没权限访问
有权限有token访问
补充
application.properties
# ┏┓ ┏┓+ + # ┏┛┻━━━┛┻┓ + + # ┃ ┃ # ┃ ━ ┃ ++ + + + # ████━████ ┃+ # ┃ ┃ + # ┃ ┻ ┃ # ┃ ┃ + + # ┗━┓ ┏━┛ # ┃ ┃ # ┃ ┃ + + + + # ┃ ┃ # ┃ ┃ + 神兽护体,代码 no bug # ┃ ┃ # ┃ ┃ + # ┃ ┗━━━┓ + + # ┃ ┣┓ # ┃ ┏┛ # ┗┓┓┏━┳┓┏┛ + + + + # ┃┫┫ ┃┫┫ # ┗┻┛ ┗┻┛+ + + + server.port=8080 server.servlet.context-path=/huangweicheng server.servlet.session.cookie.http-only=true spring.http.encoding.force=true ########################################## ####jpa连接 ## ########################################## spring.jpa.database = MYSQL spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.generate-ddl=true #数据库连接 spring.datasource.url = jdbc:mysql://127.0.0.1:3306/hwc_db?characterEncoding=utf8&useSSL=true spring.datasource.username = root spring.datasource.password = root spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect #jwt 配置 jwt.header=Authorization jwt.secret=huangweicheng jwt.expiration=1000 #reids配置 # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= #连接池最大连接数(使用负值表示没有限制) spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.lettuce.pool.max-wait=-1ms # 连接池中的最大空闲连接 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 spring.redis.lettuce.pool.min-idle=0 #日志配置 logging.path=D://log/ logging.file=huangweicheng.log logging.level.root = INFO #日志格式 logging.pattern.console=%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger- %msg%n logging.pattern.file=%d{yyyy/MM/dd-HH:mm} [%thread] %-5level %logger- %msg%n
redis相关配置
/** * * Description:redis配置,EnableCaching开启缓存 * @author huangweicheng * @date 2019/10/22 */ @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Bean @Override public KeyGenerator keyGenerator() { return (o,method,objects)-> { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(o.getClass().getName()); stringBuilder.append(method.getName()); for (Object obj : objects) { stringBuilder.append(obj.toString()); } return stringBuilder.toString(); }; } /** * * Description: redisTemplate序列化 * @param factory * @author huangweicheng * @date 2019/10/22 */ @Bean public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<Object, Object>(); redisTemplate.setConnectionFactory(factory); FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); /*设置value值的序列化*/ redisTemplate.setValueSerializer(fastJsonRedisSerializer); redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); /*设置key的序列化*/ redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setDefaultSerializer(fastJsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
数据库表
t_sys_user
t_sys_user_roles
t_sys_role
总结
jwt的token本应该是无状态的认证的,但没到过期时间这个token都是可用的,没法控制,在这期间如果被盗取,将会产生严重后果,所以引入redis控制状态。而且这还是不够严谨,应该进一步引入https的认证。增加信息的安全性,这只是一个demo,如果有需要,请留言,将会整理到码云或github上提供下载。