依賴配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.15</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
自定義全局返回結(jié)果
@Data
public class Result implements Serializable {
private int code;
private String msg;
private Object data;
public static Result succ(Object data) {
return succ(200, "操作成功", data);
}
public static Result fail(String msg) {
return fail(400, msg, null);
}
public static Result succ (int code, String msg, Object data) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
public static Result fail (int code, String msg, Object data) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
}
JWT配置類
jwt:
header: Authorization
expire: 604800 #7天,s為單位
secret: 123456
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtUtils {
private long expire;
private String secret;
private String header;
/**
* 生成JWT
* @param username
* @return
*/
public String generateToken(String username){
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
return Jwts.builder()
.setHeaderParam("typ","JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate) //7天過期
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
}
/**
* 解析JWT
* @param jwt
* @return
*/
public Claims getClaimsByToken(String jwt){
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
}catch (Exception e){
return null;
}
}
/**
* 判斷JWT是否過期
* @param claims
* @return
*/
public boolean isTokenExpired(Claims claims){
return claims.getExpiration().before(new Date());
}
}
自定義登錄處理器
/**
* 登錄成功控制器
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
//生成JWT,并放置到請求頭中
String jwt = jwtUtils.generateToken(authentication.getName());
httpServletResponse.setHeader(jwtUtils.getHeader(), jwt);
Result result = Result.succ("SuccessLogin");
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
/**
* 登錄失敗控制器
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
String errorMessage = "用戶名或密碼錯誤";
Result result;
if (e instanceof CaptchaException) {
errorMessage = "驗證碼錯誤";
result = Result.fail(errorMessage);
} else {
result = Result.fail(errorMessage);
}
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
自定義登出處理器
/**
* 登出處理器
*/
@Component
public class JWTLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
if (authentication!=null){
new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);
}
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
httpServletResponse.setHeader(jwtUtils.getHeader(), "");
Result result = Result.succ("SuccessLogout");
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
驗證碼配置
public class CaptchaException extends AuthenticationException {
public CaptchaException(String msg){
super(msg);
}
}
/**
* 驗證碼配置
*/
@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", "4");
properties.put("kaptcha.image.height", "40");
properties.put("kaptcha.image.width", "120");
properties.put("kaptcha.textproducer.font.size", "30");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
@RestController
public class CaptchaController {
@Autowired
private Producer producer;
@Autowired
private RedisUtil redisUtil;
@GetMapping("/captcha")
public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
String code = producer.createText();
BufferedImage image = producer.createImage(code);
redisUtil.set("captcha", code, 120);
// 將驗證碼圖片返回,禁止驗證碼圖片緩存
response.setHeader("Cache-Control", "no-store");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
ImageIO.write(image, "jpg", response.getOutputStream());
}
}
自定義過濾器
OncePerRequestFilter:在每次請求時只執(zhí)行一次過濾,保證一次請求只通過一次filter,而不需要重復(fù)執(zhí)行
/**
* 驗證碼過濾器
*/
@Component
public class CaptchaFilter extends OncePerRequestFilter {
@Autowired
private RedisUtil redisUtil;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String url = httpServletRequest.getRequestURI();
if ("/login/form".equals(url) && httpServletRequest.getMethod().equals("POST")) {
//校驗驗證碼
try {
validate(httpServletRequest);
} catch (CaptchaException e) {
//交給認(rèn)證失敗處理器
loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void validate(HttpServletRequest request) {
String code = request.getParameter("code");
if (StringUtils.isBlank(code)) {
throw new CaptchaException("驗證碼錯誤");
}
String captcha = (String) redisUtil.get("captcha");
if (!code.equals(captcha)) {
throw new CaptchaException("驗證碼錯誤");
}
//若驗證碼正確,執(zhí)行以下語句,一次性使用
redisUtil.del("captcha");
}
}
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<h3>表單登錄</h3>
<form method="post" th:action="@{/login/form}">
<input type="text" name="username" placeholder="用戶名"><br>
<input type="password" name="password" placeholder="密碼"><br>
<input type="text" name="code" placeholder="驗證碼"><br>
<img th:onclick="this.src='/captcha?'+Math.random()" th:src="@{/captcha}" alt="驗證碼"/><br>
<div th:if="${param.error}">
<span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用戶名或密碼錯誤</span>
</div>
<button type="submit">登錄</button>
</form>
</body>
</html>
BasicAuthenticationFilter:OncePerRequestFilter執(zhí)行完后,由BasicAuthenticationFilter檢測和處理http basic認(rèn)證,取出請求頭中的jwt,校驗jwt文章來源:http://www.zghlxwxcb.cn/news/detail-811254.html
/**
* JWT過濾器
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailServiceImpl userDetailService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = request.getHeader(jwtUtils.getHeader());
//這里如果沒有jwt,繼續(xù)往后走,因為后面還有鑒權(quán)管理器等去判斷是否擁有身份憑證,所以是可以放行的
//沒有jwt相當(dāng)于匿名訪問,若有一些接口是需要權(quán)限的,則不能訪問這些接口
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtils.getClaimsByToken(jwt);
if (claim == null) {
throw new JwtException("token異常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token已過期");
}
String username = claim.getSubject();
User user = UserDetailServiceImpl.userMap.get(username);
//構(gòu)建token,這里密碼為null,是因為提供了正確的JWT,實(shí)現(xiàn)自動登錄
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(user.getId()));
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
}
}
自定義權(quán)限異常處理器
當(dāng)BasicAuthenticationFilter認(rèn)證失敗的時候會進(jìn)入AuthenticationEntryPoint文章來源地址http://www.zghlxwxcb.cn/news/detail-811254.html
/**
* JWT認(rèn)證失敗處理器
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
Result result = Result.fail("請先登錄");
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
/**
* 無權(quán)限訪問的處理
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
Result result = Result.fail(e.getMessage());
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
自定義用戶登錄邏輯
public class AccountUser implements UserDetails {
private Long userId;
private static final long serialVersionUID = 540L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId, username, password, true, true, true, true, authorities);
}
public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
@Service
public class UserDetailServiceImpl implements UserDetailsService {
public static Map<String, User> userMap = new HashMap<>();
static {
userMap.put("root", new User("root", "123", AuthorityUtils.createAuthorityList("all")));
}
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userMap.get(s);
if (user == null) {
throw new UsernameNotFoundException("用戶名或密碼錯誤");
}
return new AccountUser(1L, user.getUsername(), passwordEncoder.encode(user.getPassword()), user.getAuthorities());
}
}
@Component
public class PasswordEncoder extends BCryptPasswordEncoder {
/**
* 加密
* @param charSequence 明文字符串
* @return
*/
@Override
public String encode(CharSequence charSequence) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return toHexString(digest.digest(charSequence.toString().getBytes()));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
}
/**
* 密碼校驗
* @param charSequence 明文,頁面收集密碼
* @param s 密文 ,數(shù)據(jù)庫中存放密碼
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(encode(charSequence));
}
/**
* @param tmp 轉(zhuǎn)16進(jìn)制字節(jié)數(shù)組
* @return 飯回16進(jìn)制字符串
*/
private String toHexString(byte [] tmp){
StringBuilder builder = new StringBuilder();
for (byte b :tmp){
String s = Integer.toHexString(b & 0xFF);
if (s.length()==1){
builder.append("0");
}
builder.append(s);
}
return builder.toString();
}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
JWTLogoutSuccessHandler jwtLogoutSuccessHandler;
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
private static final String[] URL_WHITELIST = {
"/login",
"/logout",
"/captcha",
"/favicon.ico"
};
@Bean
public PasswordEncoderImpl passwordEncoder() {
return new PasswordEncoderImpl();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable()
//登錄配置
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login/form")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
//禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//配置攔截規(guī)則
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
//異常處理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
//配置自定義的過濾器
.and()
.addFilter(jwtAuthenticationFilter())
//驗證碼過濾器放在UsernamePassword過濾器之前
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService)
.passwordEncoder(passwordEncoder());
}
/**
* 定制一些全局性的安全配置,例如:不攔截靜態(tài)資源的訪問
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 靜態(tài)資源的訪問不需要攔截,直接放行
web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
到了這里,關(guān)于SpringSecurity(07)——JWT整合的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!