一、登錄驗證碼
在jansens-admin下的pom文件添加kaptcha依賴包。
pom.xml
<!-- kaptcha --> <dependency> <groupId>com.github.axet</groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version> </dependency>
在config包下創建一個kaptcha配置類,配置驗證碼的一些生成屬性。
KaptchaConfig.java
@Configuration public class KaptchaConfig { @Bean public DefaultKaptcha producer() { Properties properties = new Properties(); properties.put("kaptcha.border", "no"); properties.put("kaptcha.textproducer.font.color", "black"); properties.put("kaptcha.textproducer.char.space", "5"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
SysLoginController.java
/** * 登錄控制器 */ @RestController public class SysLoginController { @Autowired private Producer producer; @GetMapping("captcha.jpg") public void captcha(HttpServletResponse response, HttpServletRequest request) throws ServletException, IOException { response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/jpeg"); // 生成文字驗證碼 String text = producer.createText(); // 生成圖片驗證碼 BufferedImage image = producer.createImage(text); // 保存到驗證碼到 session request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, text); ServletOutputStream out = response.getOutputStream(); ImageIO.write(image, "jpg", out); IOUtils.closeQuietly(out); } }
二、Spring Security
在jansens-admin下的pom文件中添加spring security和JWT依賴
<!-- spring security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- jwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
在config包下創建spring security的配置類
WebSecurityConfig.java
/** * Spring Security配置 */ @Configuration @EnableWebSecurity // 開啟Spring Security @EnableGlobalMethodSecurity(prePostEnabled = true) // 開啟權限注解,如:@PreAuthorize注解 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { // 使用自定義身份驗證組件 auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService)); } @Override protected void configure(HttpSecurity http) throws Exception { // 禁用 csrf, 由於使用的是JWT,我們這里不需要csrf http.cors().and().csrf().disable() .authorizeRequests() // 跨域預檢請求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // web jars .antMatchers("/webjars/**").permitAll() // 查看SQL監控(druid) .antMatchers("/druid/**").permitAll() // 首頁和登錄頁面 .antMatchers("/").permitAll() .antMatchers("/login").permitAll() // swagger .antMatchers("/swagger-ui.html").permitAll() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/v2/api-docs").permitAll() .antMatchers("/webjars/springfox-swagger-ui/**").permitAll() // 驗證碼 .antMatchers("/captcha.jpg**").permitAll() // 服務監控 .antMatchers("/actuator/**").permitAll() // 其他所有請求需要身份認證 .anyRequest().authenticated(); http.headers().frameOptions().disable(); // 退出登錄處理器 http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()); // token驗證過濾器 http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } }
新建一個security包,在其下創建JwtAuthenticationFilter並繼承BasicAuthenticationFilter,覆寫其中的doFilterInternal方法進行Token校驗
JwtAuthenticationFilter.java
/** * 登錄認證過濾器 */ public class JwtAuthenticationFilter extends BasicAuthenticationFilter { @Autowired public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 獲取token, 並檢查登錄狀態 SecurityUtils.checkAuthentication(request); chain.doFilter(request, response); } }
SecurityUtils.java
/** * 獲取令牌進行認證 * @param request */ public static void checkAuthentication(HttpServletRequest request) { // 獲取令牌並根據令牌獲取登錄認證信息 Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request); // 設置登錄認證信息到上下文 SecurityContextHolder.getContext().setAuthentication(authentication); }
JwtTokenUtils.java
/** * 根據請求令牌獲取登錄認證信息 * @return 用戶名 */ public static Authentication getAuthenticationeFromToken(HttpServletRequest request) { Authentication authentication = null; // 獲取請求攜帶的令牌 String token = JwtTokenUtils.getToken(request); if(token != null) { // 請求令牌不能為空 if(SecurityUtils.getAuthentication() == null) { // 上下文中Authentication為空 Claims claims = getClaimsFromToken(token); if(claims == null) { return null; } String username = claims.getSubject(); if(username == null) { return null; } if(isTokenExpired(token)) { return null; } Object authors = claims.get(AUTHORITIES); List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); if (authors != null && authors instanceof List) { for (Object object : (List) authors) { authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority"))); } } authentication = new JwtAuthenticatioToken(username, null, authorities, token); } else { if(validateToken(token, SecurityUtils.getUsername())) { // 如果上下文中Authentication非空,且請求令牌合法,直接返回當前登錄認證信息 authentication = SecurityUtils.getAuthentication(); } } } return authentication; }
/** * 獲取請求token * @param request * @return */ public static String getToken(HttpServletRequest request) { String token = request.getHeader("Authorization"); String tokenHead = "Bearer "; if(token == null) { token = request.getHeader("token"); } else if(token.contains(tokenHead)){ token = token.substring(tokenHead.length()); } if("".equals(token)) { token = null; } return token; } }
JwtAuthenticationProvider.java
/** * 身份驗證提供者 */ public class JwtAuthenticationProvider extends DaoAuthenticationProvider { public JwtAuthenticationProvider(UserDetailsService userDetailsService) { setUserDetailsService(userDetailsService); } @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); String salt = ((JwtUserDetails) userDetails).getSalt(); // 覆寫密碼驗證邏輯 if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } }
UserDetailsServiceImpl.java
/** * 用戶登錄認證信息查詢 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = sysUserService.findByName(username); if (user == null) { throw new UsernameNotFoundException("該用戶不存在"); } // 用戶權限列表,根據用戶擁有的權限標識與如 @PreAuthorize("hasAuthority('sys:menu:view')") 標注的接口對比,決定是否可以調用接口 Set<String> permissions = sysUserService.findPermissions(user.getName()); List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList()); return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities); } }
JwtUserDetails.java
/** * 安全用戶模型 */ public class JwtUserDetails implements UserDetails { private static final long serialVersionUID = 1L; private String username; private String password; private String salt; private Collection<? extends GrantedAuthority> authorities; JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) { this.username = username; this.password = password; this.salt = salt; this.authorities = authorities; } @Override public String getUsername() { return username; } @JsonIgnore @Override public String getPassword() { return password; } public String getSalt() { return salt; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public boolean isEnabled() { return true; } }
GrantedAuthorityImpl.java
/** * 權限封裝 */ public class GrantedAuthorityImpl implements GrantedAuthority { private static final long serialVersionUID = 1L; private String authority; public GrantedAuthorityImpl(String authority) { this.authority = authority; } public void setAuthority(String authority) { this.authority = authority; } @Override public String getAuthority() { return this.authority; } }
在SysDictController的接口添加@PreAuthorize("hasAuthority('sys:dict:view')")的注解。表示登錄的用戶擁有'sys:dict:view'權限標識才能訪問該接口。【@PreAuthorize("hasAuthority('sys:dict:add') AND hasAuthority('sys:dict:edit')")】
SwaggerConfig.java
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi(){ // 添加請求參數,我們這里把token作為請求頭部參數傳入后端 ParameterBuilder parameterBuilder = new ParameterBuilder(); List<Parameter> parameters = new ArrayList<Parameter>(); parameterBuilder.name("token").description("令牌") .modelRef(new ModelRef("string")).parameterType("header").required(false).build(); parameters.add(parameterBuilder.build()); return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select() .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()) .build().globalOperationParameters(parameters); // return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select() // .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build(); } private ApiInfo apiInfo(){ return new ApiInfoBuilder().build(); } }
三、登錄接口實現
SysLoginController.java
/** * 登錄接口 */ @PostMapping(value = "/login") public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException { String username = loginBean.getAccount(); String password = loginBean.getPassword(); String captcha = loginBean.getCaptcha(); // 從session中獲取之前保存的驗證碼跟前台傳來的驗證碼進行匹配 Object kaptcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY); if(kaptcha == null){ return HttpResult.error("驗證碼已失效"); } if(!captcha.equals(kaptcha)){ return HttpResult.error("驗證碼不正確"); } // 用戶信息 SysUser user = sysUserService.findByName(username); // 賬號不存在、密碼錯誤 if (user == null) { return HttpResult.error("賬號不存在"); } if (!PasswordUtils.matches(user.getSalt(), password, user.getPassword())) { return HttpResult.error("密碼不正確"); } // 賬號鎖定 if (user.getStatus() == 0) { return HttpResult.error("賬號已被鎖定,請聯系管理員"); } // 系統登錄認證 JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager); // 記錄登錄日志 sysLoginLogService.writeLoginLog(username, IPUtils.getIpAddr(request)); return HttpResult.ok(token); }
SecurityUtils.java
/** * 系統登錄認證 * @param request * @param username * @param password * @param authenticationManager * @return */ public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) { JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password); token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 執行登錄認證過程 Authentication authentication = authenticationManager.authenticate(token); // 認證成功存儲認證信息到上下文 SecurityContextHolder.getContext().setAuthentication(authentication); // 生成令牌並返回給客戶端 token.setToken(JwtTokenUtils.generateToken(authentication)); return token; }
JwtTokenUtils.java
/** * 生成令牌 * @return 令牌 */ public static String generateToken(Authentication authentication) { Map<String, Object> claims = new HashMap<>(3); claims.put(USERNAME, SecurityUtils.getUsername(authentication)); claims.put(CREATED, new Date()); claims.put(AUTHORITIES, authentication.getAuthorities()); return generateToken(claims); } /** * 從數據聲明生成令牌 * * @param claims 數據聲明 * @return 令牌 */ private static String generateToken(Map<String, Object> claims) { Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME); return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact(); }
Jwt工具類JwtTokenUtils.java
/** * JWT工具類 */ public class JwtTokenUtils implements Serializable { private static final long serialVersionUID = 1L; /** * 用戶名稱 */ private static final String USERNAME = Claims.SUBJECT; /** * 創建時間 */ private static final String CREATED = "created"; /** * 權限列表 */ private static final String AUTHORITIES = "authorities"; /** * 密鑰 */ private static final String SECRET = "abcdefgh"; /** * 有效期12小時 */ private static final long EXPIRE_TIME = 12 * 60 * 60 * 1000; /** * 生成令牌 * @return 令牌 */ public static String generateToken(Authentication authentication) { Map<String, Object> claims = new HashMap<>(3); claims.put(USERNAME, SecurityUtils.getUsername(authentication)); claims.put(CREATED, new Date()); claims.put(AUTHORITIES, authentication.getAuthorities()); return generateToken(claims); } /** * 從數據聲明生成令牌 * * @param claims 數據聲明 * @return 令牌 */ private static String generateToken(Map<String, Object> claims) { Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME); return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact(); } /** * 從令牌中獲取用戶名 * * @param token 令牌 * @return 用戶名 */ public static String getUsernameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 根據請求令牌獲取登錄認證信息 * @return 用戶名 */ public static Authentication getAuthenticationeFromToken(HttpServletRequest request) { Authentication authentication = null; // 獲取請求攜帶的令牌 String token = JwtTokenUtils.getToken(request); if(token != null) { // 請求令牌不能為空 if(SecurityUtils.getAuthentication() == null) { // 上下文中Authentication為空 Claims claims = getClaimsFromToken(token); if(claims == null) { return null; } String username = claims.getSubject(); if(username == null) { return null; } if(isTokenExpired(token)) { return null; } Object authors = claims.get(AUTHORITIES); List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); if (authors != null && authors instanceof List) { for (Object object : (List) authors) { authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority"))); } } authentication = new JwtAuthenticatioToken(username, null, authorities, token); } else { if(validateToken(token, SecurityUtils.getUsername())) { // 如果上下文中Authentication非空,且請求令牌合法,直接返回當前登錄認證信息 authentication = SecurityUtils.getAuthentication(); } } } return authentication; } /** * 從令牌中獲取數據聲明 * * @param token 令牌 * @return 數據聲明 */ private static Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } catch (Exception e) { claims = null; } return claims; } /** * 驗證令牌 * @param token * @param username * @return */ public static Boolean validateToken(String token, String username) { String userName = getUsernameFromToken(token); return (userName.equals(username) && !isTokenExpired(token)); } /** * 刷新令牌 * @param token * @return */ public static String refreshToken(String token) { String refreshedToken; try { Claims claims = getClaimsFromToken(token); claims.put(CREATED, new Date()); refreshedToken = generateToken(claims); } catch (Exception e) { refreshedToken = null; } return refreshedToken; } /** * 判斷令牌是否過期 * * @param token 令牌 * @return 是否過期 */ public static Boolean isTokenExpired(String token) { try { Claims claims = getClaimsFromToken(token); Date expiration = claims.getExpiration(); return expiration.before(new Date()); } catch (Exception e) { return false; } } /** * 獲取請求token * @param request * @return */ public static String getToken(HttpServletRequest request) { String token = request.getHeader("Authorization"); String tokenHead = "Bearer "; if(token == null) { token = request.getHeader("token"); } else if(token.contains(tokenHead)){ token = token.substring(tokenHead.length()); } if("".equals(token)) { token = null; } return token; } }
LoginBean.java
/** * 登錄接口封裝對象 */ public class LoginBean { private String account; private String password; private String captcha; public String getAccount() { return account; } public void setAccount(String account) { this.account = account; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getCaptcha() { return captcha; } public void setCaptcha(String captcha) { this.captcha = captcha; } }
JwtAuthenticatioToken.java
/** * 自定義令牌對象 */ public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken { private static final long serialVersionUID = 1L; private String token; public JwtAuthenticatioToken(Object principal, Object credentials){ super(principal, credentials); } public JwtAuthenticatioToken(Object principal, Object credentials, String token){ super(principal, credentials); this.token = token; } public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) { super(principal, credentials, authorities); this.token = token; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public static long getSerialversionuid() { return serialVersionUID; } }