在生產環境中,對發在的API增加授權保護是非常必要的。JWT作為一個無狀態的授權校撿技術,非常適合於分散式系統架構。伺服器端不需要保存用戶狀態,因此,無須採用Redis等技術來實現各個服務節點之間共用Session數據。 本節通過實例講解如何用JWT技術進行授權認證和保護。 1.1 配置安全類 (1 ...
在生產環境中,對發在的API增加授權保護是非常必要的。JWT作為一個無狀態的授權校撿技術,非常適合於分散式系統架構。伺服器端不需要保存用戶狀態,因此,無須採用Redis等技術來實現各個服務節點之間共用Session數據。
本節通過實例講解如何用JWT技術進行授權認證和保護。
1.1 配置安全類
(1)自定義用戶
查看代碼
package com.intehel.jwt.domain;
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;
@Entity
@Data
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
@ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
(2)自定義角色
查看代碼
package com.intehel.jwt.domain;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Data
@Entity(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String nameZh;
}
(3)JPA
package com.intehel.jwt.repository;
import com.intehel.jwt.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User,Integer> {
User findUserByUsername(String username);
}
package com.intehel.jwt.repository;
import com.intehel.jwt.domain.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRoleRepository extends JpaRepository<Role,Integer> {
Role findByName(String name);
}
(4)認證失敗和認證成功處理器
查看代碼
package com.intehel.jwt.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Author:李自航
* @Description:
* @CreateDate:2022/8/5 10:15
* @UPdateDate:2022/8/5 10:15
* @Version:版本號
*/
@Component
public class JwtAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
String name = request.getParameter("name");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\n" +
"\t\"status\":\"error\",\n" +
"\t\"message\":\"用戶名或密碼錯誤\"\n" +
"}");
out.flush();
out.close();
}
}
查看代碼
package com.intehel.jwt.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
@Component
public class JwtAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal != null && principal instanceof UserDetails){
UserDetails user = (UserDetails) principal;
request.getSession().setAttribute("userDetail",user);
String role = "";
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
for (GrantedAuthority authority : authorities){
role = authority.getAuthority();
}
String token = "灌水灌水";
response.setHeader("token",token);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\n" +
"\t\"status\":\"ok\",\n" +
"\t\"message\":\"登錄成功\"\n" +
"}\n");
out.flush();
out.close();
}
}
}
(5)配置安全類
package com.intehel.jwt.config;
import com.intehel.jwt.handler.JwtAuthenticationFailHandler;
import com.intehel.jwt.handler.JwtAuthenticationSuccessHandler;
import com.intehel.jwt.handler.MyAuthenticationFailureHandler;
import com.intehel.jwt.handler.MyAuthenticationSuccessHandler;
import com.intehel.jwt.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private JwtAuthenticationFailHandler myAuthenticationFailureHandler;
@Autowired
MyUserDetailsService jwtDetailsService;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/jwt/**")
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/doLogin")
.loginPage("/mylogin.html")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/register/mobile").permitAll()
.antMatchers("/article/**").authenticated()
.antMatchers("/jwt/tasks/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.csrf().disable();
http.logout().permitAll();
http.cors().and().csrf().ignoringAntMatchers("/jwt/**");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/jwt/register/mobile");
}
}
從上面代碼可以看出,此處JWT的安全配置和上面已經講解過的安全配置並無區別,沒有特別的參數需要配置。
1.2 自定義登錄界面
查看代碼
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
#login .container #login-row #login-column #login-box {
border: 1px solid #9c9c9c;
background-color: #EAEAEA;
}
</style>
<body>
<div id="login">
<div class="container">
<div id="login-row" class="row justify-content-center align-items-center">
<div id="login-column" class="col-md-6">
<div id="login-box" class="col-md-12">
<form id="login-form" class="form" action="/doLogin" method="post">
<h3 class="text-center text-info">登錄</h3>
<!--/*@thymesVar id="SPRING_SECURITY_LAST_EXCEPTION" type="com"*/-->
<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
<div class="form-group">
<label for="username" class="text-info">用戶名:</label><br>
<input type="text" name="username" id="username" class="form-control">
</div>
<div class="form-group">
<label for="password" class="text-info">密碼:</label><br>
<input type="text" name="password" id="password" class="form-control">
</div>
<div class="form-group">
<input type="submit" name="submit" class="btn btn-info btn-md" value="登錄">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
1.3 處理註冊
在註冊時為了安全,需要將註冊的密碼經過加密再寫入資料庫中。
spring security 5之後,需要對密碼添加這個類型(id),可參考文章www.cnblogs.com/majianming/p/7923604.html
查看代碼
package com.intehel.jwt.controller;
import com.intehel.jwt.domain.Role;
import com.intehel.jwt.domain.User;
import com.intehel.jwt.repository.UserRepository;
import com.intehel.jwt.repository.UserRoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/jwt")
public class JwtUserController {
@Autowired
private UserRepository userRepository;
@Autowired
private UserRoleRepository userRoleRepository;
@RequestMapping(value = "/register/mobile")
public String register(User user){
try {
User userName = userRepository.findUserByUsername(user.getUsername());
if (userName != null){
return "用戶名已存在";
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
user.setPassword("{bcrypt}"+encoder.encode(user.getPassword()));
List<Role> roles = new ArrayList<>();
Role role = userRoleRepository.findByName("ROLE_admin");
roles.add(role);
user.setRoles(roles);
userRepository.save(user);
}catch (Exception e){
return "出現了異常";
}
return "成果";
}
}
1.4 處理登錄
查看代碼
package com.intehel.jwt.service;
import com.intehel.jwt.domain.User;
import com.intehel.jwt.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)throws UsernameNotFoundException {
User user = userRepository.findUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用戶不存在");
}
return user;
}
}
測試多方式註冊和登錄
1.測試註冊功能
這裡使用測試工具Postman提交POST註冊請求
資料庫插入信息如下
2. 測試登錄功能
瀏覽器輸入http://localhost:8080/jwt自動跳轉至登錄界面,輸入使用postman註冊的賬號即可
以本博客對spring security的隨筆,可實現使用token授權登錄,這裡不多做解釋