概述
基於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上提供下載。