一句簡潔明了的權限開發的名句:要對Web資源進行保護,最好的辦法莫過于Filter;要想對方法調用進行保護,最好的辦法莫過于AOP
文章目錄
- 1 Spring Security 介紹
- Spring Security 簡介
- Spring Security 對比 Apache Shiro
- SpringSecurity 架構
- 2 項目實戰
- 使用者認證
- 流程分析
- 編碼
- Debug 分析(超詳細)
- 使用者授權
1 Spring Security 介紹
Spring Security 簡介
Spring 是非常流行和成功的 Java 應用開發架構,Spring Security 正是 Spring 家族中的成員。Spring Security 基于 Spring 架構,提供了一套 Web 應用安全性的完整解決方案。
Spring Security 對比 Apache Shiro
SpringSecurity 特點:
- 和 Spring 無縫整合
- 全面的權限控制
- 專門為 Web 開發而設計。舊版本不能脫離 Web 環境使用。新版本對整個架構進行了分層抽取,分成了核心子產品和 Web 子產品。單獨引入核心子產品就可以脫離 Web 環境
- 重量級
Apache Shiro 特點:
- 輕量級。Shiro 主張的理念是把複雜的事情變簡單。針對對性能有更高要求的網際網路應用有更好表現
- 通用性。好處:不局限于 Web 環境,可以脫離 Web 環境使用。缺陷:在 Web 環境下一些特定的需求需要手動編寫代碼定制
SpringSecurity 架構
Spring Security進行認證和鑒權的時候,就是利用的一系列的Filter來進行攔截的。如圖所示,一個請求想要通路到API就會從左到右經過藍線框裡的過濾器,其中綠色部分是負責認證的過濾器,藍色部分是負責異常處理,橙色部分則是負責授權。進過一系列攔截最終通路到我們的API。這裡面我們隻需要重點關注兩個過濾器即可:
UsernamePasswordAuthenticationFilter
負責登入認證,
FilterSecurityInterceptor
負責權限授權。
2 項目實戰
大緻流程如下圖
引入依賴
<!-- Spring Security依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided </scope>
</dependency>
使用者認證
流程分析
來到 UsernamePasswordAuthenticationFilter 的父類 AbstractAuthenticationProcessingFilter
父類中的 doFilter 就是做上述過程的核心方法
其中 attemptAuthentication 為主要認證過程,其細節依賴于 AbstractAuthenticationProcessingFilter 的最終子類是否重寫這個方法
最終調用 Provider 的 authenticate 方法進行認證
最後将認證結果放入上下文
編碼
1 自定義:加密處理元件
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
2 自定義:使用者實體類
public class CustomUser extends User {
/**
* 我們自己的使用者實體對象,要調取使用者資訊時直接擷取這個實體對象
*/
private SysUser sysUser;
public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
super(sysUser.getUsername(), sysUser.getPassword(), authorities);
this.sysUser = sysUser;
}
public SysUser getSysUser() {
return sysUser;
}
public void setSysUser(SysUser sysUser) {
this.sysUser = sysUser;
}
3 自定義:根據使用者名查使用者資訊
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if(null == sysUser) {
throw new UsernameNotFoundException("使用者名不存在!");
}
if(sysUser.getStatus().intValue() == 0) {
throw new RuntimeException("賬号已停用");
}
return new CustomUser(sysUser, Collections.emptyList());
}
}
4 自定義:登入過濾器
/**
* <p>
* 登入過濾器,繼承UsernamePasswordAuthenticationFilter,對使用者名密碼進行登入校驗
* </p>
*
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
public TokenLoginFilter(AuthenticationManager authenticationManager) {
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
//指定登入接口及送出方式,可以指定任意路徑
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
}
/**
* 登入認證
* @param req
* @param res
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 登入成功
* @param request
* @param response
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
CustomUser customUser = (CustomUser) auth.getPrincipal();
String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
Map<String, Object> map = new HashMap<>();
map.put("token", token);
ResponseUtil.out(response, Result.ok(map));
}
/**
* 登入失敗
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
if(e.getCause() instanceof RuntimeException) {
ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
}
}
}
工具類 ResponseUtil
public class ResponseUtil {
public static void out(HttpServletResponse response, Result r) {
ObjectMapper mapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try {
mapper.writeValue(response.getWriter(), r);
} catch (IOException e) {
e.printStackTrace();
}
}
}
5 自定義:認證過濾器
/**
* <p>
* 認證解析token過濾器
* </p>
*/
public class TokenAuthenticationFilter extends OncePerRequestFilter {
public TokenAuthenticationFilter() {
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("uri:"+request.getRequestURI());
//如果是登入接口,直接放行
if("/admin/system/index/login".equals(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if(null != authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// token置于header裡
String token = request.getHeader("token");
logger.info("token:"+token);
if (!StringUtils.isEmpty(token)) {
String useruame = JwtHelper.getUsername(token);
logger.info("useruame:"+useruame);
if (!StringUtils.isEmpty(useruame)) {
return new UsernamePasswordAuthenticationToken(useruame, null, Collections.emptyList());
}
}
return null;
}
}
6 自定義:SpringSecurity 配置類
@Configuration
@EnableWebSecurity //@EnableWebSecurity是開啟SpringSecurity的預設行為
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟注解功能,預設禁用注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomMd5Password customMd5PasswordEncoder;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private LoginLogService loginLogService;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 這是配置的關鍵,決定哪些接口開啟防護,哪些接口繞過防護
http
//關閉csrf
.csrf().disable()
// 開啟跨域以便前端調用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通過驗證即可通路。登陸接口肯定是不需要認證的
//.antMatchers("/admin/system/index/login").permitAll()
// 這裡意思是其它所有接口需要認證才能通路
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,
// 這樣做就是為了除了登入的時候去查詢資料庫外,其他時候都用token進行認證。
.addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate,loginLogService));
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService和加密器
auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);
}
/**
* 配置哪些請求不攔截
* 排除swagger相關請求
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
}
}
Debug 分析(超詳細)
再放一次流程拓撲圖
發起登入請求
首先來到我們自定義的認證過濾器,由于是管理者登入接口的請求路徑,是以直接放行,否則就走下面的擷取 header 中 token 的方法(從緩存中查此 token 中包含的 useruame 對應的權限清單),校驗是否正确并存在
之後來到了登入過濾器,建立出 UsernamePasswordAuthenticationToken
之後走 authenticate 方法對密碼進行加密
之後來到之前自定義的根據使用者名查使用者資訊的實作類
同時擷取了使用者權限的相關資訊(這裡是測試時用的代碼,可以删掉)
之後才走密碼的校驗
最後來到了登入過濾器的成功響應方法
成功擷取到 token
使用者授權
每次請求進來,進入認證過濾器,由于不是登入請求,是以會被攔截判斷 header 中的 token 是否正确,以及對應解析出來的使用者名中是否在緩存中對應權限清單