2.1整合springsecurity
1)添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2認證授權流程
認證管理
流程圖解讀:
1、使用者送出使用者名、密碼被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 過濾器擷取到, 封裝為請求Authentication,通常情況下是UsernamePasswordAuthenticationToken這個實作類。
2、然後過濾器将Authentication送出至認證管理器(AuthenticationManager)進行認證 。
3、認證成功後, AuthenticationManager 身份管理器傳回一個被填充滿了資訊的(包括上面提到的權限資訊, 身份資訊,細節資訊,但密碼通常會被移除) Authentication 執行個體。
4、SecurityContextHolder 安全上下文容器将第3步填充了資訊的 Authentication ,通過 SecurityContextHolder.getContext().setAuthentication(…)方法,設定到其中。可以看出AuthenticationManager接口(認證管理器)是認證相關的核心接口,也是發起認證的出發點,它 的實作類為ProviderManager。而Spring Security支援多種認證方式,是以ProviderManager維護着一個 List 清單,存放多種認證方式,最終實際的認證工作是由 AuthenticationProvider完成的。咱們知道web表單的對應的AuthenticationProvider實作類為 DaoAuthenticationProvider,它的内部又維護着一個UserDetailsService負責UserDetails的擷取。最終 AuthenticationProvider将UserDetails填充至Authentication。
授權管理
通路資源(即授權管理),通路url時,會通過FilterSecurityInterceptor攔截器攔截,其中會調用SecurityMetadataSource的方法來擷取被攔截url所需的全部權限,再調用授權管理器AccessDecisionManager,這個授權管理器會通過spring的全局緩存SecurityContextHolder擷取使用者的權限資訊,還會擷取被攔截的url和被攔截url所需的全部權限,然後根據所配的投票政策(有:一票決定,一票否定,少數服從多數等),如果權限足夠,則決策通過,傳回通路資源,請求放行,否則跳轉到403頁面、自定義頁面。
2.3編寫自己的UserDetails和UserDetailService
2.3.1UserDetails
package com.ds.book.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Collection;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
* <p>
*
* </p>
*
* @author java大師
* @since 2023-03-17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_user")
public class User implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
private Integer id;
/**
* 登入名
*/
private String name;
/**
* 使用者名
*/
private String username;
/**
* 密碼
*/
private String password;
/**
* 是否有效:1-有效;0-無效
*/
private String status;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles
.stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleCode()))
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2.3.2userDetailService
登入成功後,将UserDetails的roles設定到使用者中
package com.ds.book.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ds.book.entity.User;
import com.ds.book.mapper.UserMapper;
import com.ds.book.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
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;
/**
* <p>
* 服務實作類
* </p>
*
* @author java大師
* @since 2023-03-17
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User loginUser = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (loginUser == null){
throw new UsernameNotFoundException("使用者名或密碼錯誤");
}
loginUser.setRoles(userMapper.getRolesByUserId(loginUser.getId()));
return loginUser;
}
}
2.3.2加載userDetailService
将我們自己的UserDetailService注入springsecurity
package com.ds.book.config;
import com.ds.book.filter.JwtTokenFilter;
import com.ds.book.service.impl.UserServiceImpl;
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.ObjectPostProcessor;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userService;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//注入我們自己的UserDetailService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
}
問題:前後端分離項目,通常不會使用springsecurity自帶的登入界面,登入界面由前端完成,背景隻需要提供響應的服務即可,且目前主流不會采用session去存取使用者,後端會傳回響應的token,前端通路的時候,會在headers裡面帶入token.
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-pW7SxQqz-1679881634010)(D:\個人\公衆号\網站\dsblog開發手冊\image-20230322114008186.png)]
2.4JwtToken
2.4.1 JWT描述
Jwt token由Header、Payload、Signature三部分組成,這三部分之間以小數點”.”連接配接,JWT token長這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU
token解析後長這樣:
header部分,有令牌的類型(JWT)和簽名算法名稱(HS256):
{
“alg”: “HS256”,
“typ”: “JWT”
}
Payload部分,有效負載,這部分可以放任何你想放的資料:
{
“sub”: “1234567890”,
“name”: “John Doe”,
“iat”: 1516239022
}
Signature簽名部分,由于這部分是使用header和payload部分計算的,是以還可以以此來驗證payload部分有沒有被篡改:
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
123456 //這裡是密鑰,隻要夠複雜,一般不會被破解
)
2.4.2 pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.4.3 JwtToken工具類
package com.ds.book.tool;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具類
*/
public class JwtUtil {
//有效期為
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一個小時
//設定秘鑰明文
public static final String JWT_KEY = "dashii";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的資料(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 設定過期時間
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的資料(json格式)
* @param ttlMillis token逾時時間
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 設定過期時間
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis= JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主題 可以是JSON資料
.setIssuer("dashi") // 簽發者
.setIssuedAt(now) // 簽發時間
.signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個參數為秘鑰
.setExpiration(expDate);
}
/**
* 建立token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 設定過期時間
return builder.compact();
}
/**
* 生成加密後的秘鑰 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
2.4.4 JwtTokenFilter
package com.ds.book.filter;
import com.ds.book.entity.User;
import com.ds.book.mapper.UserMapper;
import com.ds.book.service.IMenuService;
import com.ds.book.service.IUserService;
import com.ds.book.tool.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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;
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private IUserService userService;
@Autowired
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//1、擷取token
String token = httpServletRequest.getHeader("token");
if (StringUtils.isEmpty(token)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception exception) {
exception.printStackTrace();
throw new RuntimeException("token非法");
}
User user = userService.getUserById(Integer.parseInt(userId));
user.setRoles(userMapper.getRolesByUserId(Integer.parseInt(userId)));
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
在springsecurity中,第一個經過的過濾器是UsernamePasswordAuthenticationFilter,是以前後端分離的項目,我們自己定義的過濾器要放在這個過濾器前面,具體配置如下
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.cors();
}
2.4.5授權
2.4.5.1 開啟preAuthorize進行收取(Controller路徑比對)
1)主啟動類上添加EnableGlobalMethodSecurity注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
@SpringBootApplication
@MapperScan("com.ds.book.mapper")
public class BookSysApplication {
public static void main(String[] args) {
SpringApplication.run(BookSysApplication.class,args);
}
}
2)Controller方法上添加@PreAuthorize注解
@RestController
public class HelloController {
@GetMapping("/hello")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String hello(){
return "hello";
}
}
2.4.5.2 增強方式授權(資料庫表配置)
1)建立我們自己的FilterInvocationSecurityMetadataSource,實作getAttributes方法,擷取請求url所需要的角色
@Component
public class MySecurtiMetaDataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private IMenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
//擷取通路url需要的角色,例如:/sys/user需要ROLE_ADMIN角色,通路sys/user時擷取到必須要有ROLE_ADMIN角色。傳回 Collection<ConfigAttribute>
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
//擷取所有的菜單及角色
List<Menu> menus = menuService.getMenus();
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getUrl(),requestURI)){
String[] roles = menu.getRoles().stream().map(role -> role.getRoleCode()).toArray(String[]::new);
return SecurityConfig.createList(roles);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
2)建立我們自己的決策管理器AccessDecisionManager,實作decide方法,判斷步驟1)中擷取到的角色和我們目前登入的角色是否相同,相同則允許通路,不相同則不允許通路,
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
//1、認證通過後,會往authentication中填充使用者資訊
//2、拿authentication中的權限與上一步擷取到的角色資訊進行比對,比對成功後,允許通路
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute configAttribute : configAttributes) {
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(configAttribute.getAttribute())){
return;
}
}
}
throw new AccessDeniedException("權限不足,請聯系管理者");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return false;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
3)在SecurityConfig中,添加後置處理器(增強器),讓springsecurity使用我們自己的datametasource和decisionMananger
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MySecurtiMetaDataSource mySecurtiMetaDataSource;
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private UserServiceImpl userService;
@Autowired
private JwtTokenFilter jwtTokenFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
//後置處理器,使用我們自己的FilterSecurityInterceptor攔截器配置
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor> () {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(mySecurtiMetaDataSource);
o.setAccessDecisionManager(myAccessDecisionManager);
return o;
}
})
.and()
.headers().cacheControl();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.cors();
}
}
2.4.6異常處理
1)前端渲染工具類
public class WebUtils
{
/**
* 将字元串渲染到用戶端
*
* @param response 渲染對象
* @param string 待渲染的字元串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
2)未登入異常處理,實作commence方法
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Result result = new Result(401,"未登入,請先登入",null);
String json = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,json);
}
}
3)授權失敗異常處理,實作Handle方法
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
Result result = new Result(403,"權限不足請聯系管理者",null);
String s = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,s);
}
}
原文連結;https://mp.weixin.qq.com/s/39z49TnZ21Wkyz8jhf_XGQ