spring-security-demo
前言:本來是想盡量簡單簡單點的寫一個demo的,但是spring-security實在是內容有點多,寫着寫着看起來就沒那么簡單了,想入門spring-security的話還是需要下些功夫的,這遠沒有Mybatis、JPA之類的容易入門
一個spring-security采用jwt認證機制的demo。
以下代碼僅為說明代碼作用,有的並不完整,如若要參考請git clone整個項目代碼查看
參考:
spring security學習(SpringBoot2.1.5版本)
SpringBootSecurity學習(13)前后端分離版之JWT
重拾后端之Spring Boot(四):使用JWT和Spring Security保護REST API
spring-security
config.securityConfig是springSecurity的安全配置類,在這個類中配置需要驗證的接口、需要放行的接口,配置登錄成功失敗的處理器
1.最簡單的用戶角色權限控制demo
最簡單是demo是直接在securityConfig中配置存在內存中的用戶對象,可以采用一下代碼配置用戶角色:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user").password({noop}123).roles("USER")
.and()
.withUser("admin").password({noop}123).roles("ADMIN")
.and()
.withUser("one").password({noop}123).roles("ONE")
.and()
.withUser("two").password({noop}123).roles("TWO");
}
然后在securityConfig加注解開啟接口的preAuth注解支持
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true,jsr250Enabled = true)
public class securityConfig extends WebSecurityConfigurerAdapter {
然后可以直接在Controller的接口上加注解
/* 只有角色ONE才能訪問 */
@PreAuthorize("hasRole('ONE')")
@GetMapping("/hello")
public String hello(){
return "hello Spring Security";
}
然后訪問localhost:8080/two,發現會跳轉到login登錄頁面,此時以one登錄進去可以正常訪問,但是以其它角色訪問均會出錯。至此,最簡單的demo已完成。
2.修改用戶為數據庫用戶
上面的用戶是存在內存中的,接下來需要將其改為從數據庫中獲取用戶信息並驗證。
首先需要在securityConfig中配置spring-security加載用戶時使用的類,spring-security通過我們提供的這個類得到一個用戶信息,該用戶信息中一般包含用戶名、密碼、角色,spring-security得到這些信息后完成后續操作。
提供該類給securityConfig
@Qualifier("userDetailServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService) // 提供給spring-security的類
.passwordEncoder( new BCryptPasswordEncoder() ); // 這是密碼加密的類,可以理解為將明文密碼加密成hash值,可以先忽略照寫
}
然后需要實現這個類
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private UserPasswordRepository userPasswordRepository;
@Autowired
private UserRoleRepository userRoleRepository;
/**
* 我的數據庫表分為User表、UserInfo用戶詳細信息表、UserPassword密碼表、UserRole用戶角色表
* spring-security會給這個方法提供一個用戶名,然后我們實現根據用戶名得到這個用戶的UserDetail信息(類似於包含用戶名、密碼、角色的實體類,下一步重寫它)
* 然后返回的就是這個UserDetail,spring-security可以使用該類完成其它的操作
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findFirstByUsername(username);
Integer id = user.getId();
if (Objects.nonNull(user) && username.trim().length() <= 0) {
throw new UsernameNotFoundException("用戶名錯誤");
}
// 填充所有角色信息
List<GrantedAuthorityImpl> grantedAuthorities = new ArrayList<>();
List<UserRole> roles = userRoleRepository.findByCreator_Id(id);
for (UserRole role : roles) {
grantedAuthorities.add(new GrantedAuthorityImpl("ROLE_" + role.getRole()));
}
return new UserDetailImpl(
username,
userPasswordRepository.findByCreator_Id(id).getPassword(),
grantedAuthorities
);
}
}
實現UserDetail,這個類就像是一個實體類,但是實現了UserDetails接口,遵循spring-security的規范以讓spring-security能使用它
@NoArgsConstructor
@ToString
public class UserDetailImpl implements UserDetails {
private String username;
@JsonIgnore
private String password;
private List<GrantedAuthorityImpl> authorities;
@JsonIgnore
private boolean accountNonExpired;
@JsonIgnore
private boolean accountNonLocked;
@JsonIgnore
private boolean credentialsNonExpired;
@JsonIgnore
private boolean enabled;
public UserDetailImpl(String username, String password, List<GrantedAuthorityImpl> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
this.accountNonExpired = true;
this.accountNonLocked = true;
this.credentialsNonExpired = true;
this.enabled = true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
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;
}
}
最好再實現一下GrantedAuthority,這個是角色信息的規范接口
@NoArgsConstructor
public class GrantedAuthorityImpl implements GrantedAuthority {
private String authority;
public GrantedAuthorityImpl(String authority) {
this.authority = authority;
}
@Override
public String getAuthority() {
return authority;
}
}
上述這些就完成了spring-security用戶表轉移到數據庫的操作了。
3.引入jwt
上述過程中,spring-security默認使用session-cookie的方法保存一個連接中的用戶信息,然后拿這些用戶信息到數據庫查詢。接下來可以改造成為jwt保存用戶信息,jwt其實就是平時經常看到的token保存用戶信息,其機制是直接將用戶信息寫在token中,然后就這個token進行簽名后頒發給用戶,用戶發起請求時可以攜帶token,服務器就可以直接給用戶認證信息了。
首先我們先來構造jwt token。
首先是jwt的工具類,該類提供信息HMACSHA256加密、信息簽名、測試token是否合法
public class JWTUtils {
public static final String DEFAULT_HEADER = "\"alg\":\"HS256\",\"typ\":\"JWT\"";
public static final String SECRET = "woshizengchunmiao";
public static final long EXPIRE_TIME = 1000 * 60 * 60 * 24;
public static final String HEADER_TOKEN_NAME = "Authrization";
public static String encode(String input) {
return Base64.getEncoder().encodeToString(input.getBytes());
}
public static String decode(String input) {
return new String(Base64.getDecoder().decode(input));
}
public static String HMACSHA256(String data, String secret) throws Exception {
Mac hmacSHA256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256");
hmacSHA256.init(secretKeySpec);
byte[] bytes = hmacSHA256.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte aByte : bytes) {
sb.append(Integer.toHexString((aByte & 0xFF) | 0x100), 1, 3);
}
return sb.toString().toUpperCase();
}
public static String getSignature(String payload) throws Exception {
return HMACSHA256(encode(DEFAULT_HEADER) + encode(payload), SECRET);
}
public static String testJwt(String jwt) {
String[] split = jwt.split("\\.");
try {
if (!(HMACSHA256(split[0] + split[1], SECRET).equals(split[2]))) {
return null;
}
if (!decode(split[0]).equals(DEFAULT_HEADER)) {
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
return decode(split[1]);
}
}
然后提供一個JWT類,構造該類時,只需要將想放在token上的信息傳入構造函數,即可得到一個想要的JWT,調用toString方法就得到了token
public class JWT {
private String header;
private String payload;
private String signature;
public JWT(String payload) throws Exception {
this.payload = JWTUtils.encode(payload);
this.header = JWTUtils.encode(JWTUtils.DEFAULT_HEADER);
this.signature = JWTUtils.getSignature(payload);
}
@Override
public String toString() {
return header + "." + payload + "." + signature;
}
}
4.jwt設置到spring-security
以上兩個類就完成了token的構造,然后我們需要用它來代替spring-security中的session-cookie機制。首先需要將spring-security的session關閉,實質上我的理解是,token是一個虛擬的session,每次建立連接時,spring-security將它解析出來把它作為認證信息放到Holder里。
關閉session,在securityConfig
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 設置無session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
然后需要寫一個Filter,在spring-security進行用戶名-密碼驗證前搶先發生,對token進行驗證,若token合法就放入認證信息,就完成了安全認證;若token不合法直接失敗。
先配置這個Filter到config中
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 攔截登錄請求
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
然后實現這個Filter
/**
* 驗證token是否正確,並從token中還原"session"信息
*/
public class JwtAuthenticationFilter extends GenericFilter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = request.getHeader(JWTUtils.HEADER_TOKEN_NAME); // 從請求頭中拿到token
if (Objects.nonNull(token) && token.trim().length() > 0) {
String payload = JWTUtils.testJwt(token); // 從token中拿到payload
if (Objects.nonNull(payload) && payload.trim().length() > 0) {
ObjectMapper objectMapper = new ObjectMapper();
// 我這個項目的payload是UserDetailImp的序列化后的Json,這里將其還原為UserDetailImpl對象
UserDetailImpl user = objectMapper.readValue(payload, UserDetailImpl.class);
// 將還原得到的認證信息交給spring-security管理(用戶信息,認證,用戶角色表)
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()));
}
}
filterChain.doFilter(servletRequest,servletResponse);
}
}
以上,就完成了spring-security使用JWT的全部過程。可以測試使用了
為了方便測試,我還提供了SuccessHandle、FailureHandle、AccessDeniedHandlerImpl用於spring-security登錄成功、登錄失敗、沒有認證信息的處理器,其中,SuccessHandle在登錄成功后返回當前認證信息的token,拿這個token放到請求頭訪問接口時,即可自動完成認證。
測試
提供了一個hello的Controller層接口
@RestController
public class hello {
// 擁有ADMIN角色才可以訪問
@PreAuthorize("hasAnyRole('ADMIN')")
@RequestMapping("/hello")
String test() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof UserDetailImpl) {
UserDetailImpl user = (UserDetailImpl) authentication.getPrincipal();
System.out.println(user.getUsername());
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
System.out.println(authority.getAuthority());
}
}
return "hello";
}
}
直接訪問該接口,由於沒有認證,會跳轉到/login接口下

以admin-123登錄后跳轉成功頁面並得到token

復制token,放到postman的header里,然后再次請求/hello

發現成功得到響應

到控制台看看,得到用戶名和角色名

換user-123角色的token登錄看看


還可以更換root角色,不再贅述。
