演示地址:http://139.196.87.48:9002/kittyhtml
用戶名:admin 密碼:admin前端
到目前爲止,咱們使用的權限認證框架是 Shiro,雖然 Shiro 也足夠好用而且簡單,但對於 Spring 官方主推的安全框架 Spring Security,用戶羣也是甚大的,因此咱們這裏把當前的代碼切分出一個 shiro-cloud 分支,做爲 Shiro + Spring Cloud 技術的分支代碼,dev 和 master 分支將替換爲 Spring Security + Spring Cloud 的技術棧,並在後續計劃中集成 Spring Security OAuth2 實現單點登陸功能。java
移除shiro依賴,添加Spring Scurity和JWT依賴包,jwt目前的最新版本是0.9.1。git
<!-- spring security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- jwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jwt.version}</version> </dependency>
替換Shiro的權限註解爲Spring Security的權限註解。web
格式以下:spring
@PreAuthorize("hasAuthority('sys:menu:view')")json
SysMenuController.java後端
package com.louis.kitty.admin.controller; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.louis.kitty.admin.model.SysMenu; import com.louis.kitty.admin.sevice.SysMenuService; import com.louis.kitty.core.http.HttpResult; /** * 菜單控制器 * @author Louis * @date Oct 29, 2018 */ @RestController @RequestMapping("menu") public class SysMenuController { @Autowired private SysMenuService sysMenuService; @PreAuthorize("hasAuthority('sys:menu:add') AND hasAuthority('sys:menu:edit')") @PostMapping(value="/save") public HttpResult save(@RequestBody SysMenu record) { return HttpResult.ok(sysMenuService.save(record)); } @PreAuthorize("hasAuthority('sys:menu:delete')") @PostMapping(value="/delete") public HttpResult delete(@RequestBody List<SysMenu> records) { return HttpResult.ok(sysMenuService.delete(records)); } @PreAuthorize("hasAuthority('sys:menu:view')") @GetMapping(value="/findNavTree") public HttpResult findNavTree(@RequestParam String userName) { return HttpResult.ok(sysMenuService.findTree(userName, 1)); } @PreAuthorize("hasAuthority('sys:menu:view')") @GetMapping(value="/findMenuTree") public HttpResult findMenuTree() { return HttpResult.ok(sysMenuService.findTree(null, 0)); } }
Spring Security註解默認是關閉的,能夠經過在配置類添加如下註解開啓。api
@EnableGlobalMethodSecurity(prePostEnabled = true)
添加安全配置類, 繼承 WebSecurityConfigurerAdapter,配置URL驗證策略和相關過濾器以及自定義的登陸驗證組件。跨域
WebSecurityConfig.java
package com.louis.kitty.admin.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; import com.louis.kitty.admin.security.JwtAuthenticationFilter; import com.louis.kitty.admin.security.JwtAuthenticationProvider; /** * Spring Security Config * @author Louis * @date Nov 20, 2018 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { // 使用自定義身份驗證組件 auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService)); } @Override protected void configure(HttpSecurity http) throws Exception { // 禁用 csrf, 因爲使用的是JWT,咱們這裏不須要csrf http.cors().and().csrf().disable() .authorizeRequests() // 跨域預檢請求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // web jars .antMatchers("/webjars/**").permitAll() // 查看SQL監控(druid) .antMatchers("/druid/**").permitAll() // 首頁和登陸頁面 .antMatchers("/").permitAll() .antMatchers("/login").permitAll() // swagger .antMatchers("/swagger-ui.html").permitAll() .antMatchers("/swagger-resources").permitAll() .antMatchers("/v2/api-docs").permitAll() .antMatchers("/webjars/springfox-swagger-ui/**").permitAll() // 驗證碼 .antMatchers("/captcha.jpg**").permitAll() // 服務監控 .antMatchers("/actuator/**").permitAll() // 其餘全部請求須要身份認證 .anyRequest().authenticated(); // 退出登陸處理器 http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()); // 登陸認證過濾器 http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } }
繼承 DaoAuthenticationProvider, 實現自定義的登陸驗證組件,覆寫密碼驗證邏輯。
JwtAuthenticationProvider.java
package com.louis.kitty.admin.security; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import com.louis.kitty.admin.util.PasswordEncoder; /** * 身份驗證提供者 * @author Louis * @date Nov 20, 2018 */ public class JwtAuthenticationProvider extends DaoAuthenticationProvider { public JwtAuthenticationProvider(UserDetailsService userDetailsService) { setUserDetailsService(userDetailsService); } @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); String salt = ((JwtUserDetails) userDetails).getSalt(); // 覆寫密碼驗證邏輯 if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } }
實現 UserDetailsService 接口,定義用戶認證信息查詢組件,用於獲取認證所需的用戶信息和受權信息。
UserDetailsServiceImpl.java
package com.louis.kitty.admin.security; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; 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; import com.louis.kitty.admin.model.SysUser; import com.louis.kitty.admin.sevice.SysUserService; /** * 用戶登陸認證信息查詢 * @author Louis * @date Nov 20, 2018 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = sysUserService.findByName(username); if (user == null) { throw new UsernameNotFoundException("該用戶不存在"); } // 用戶權限列表,根據用戶擁有的權限標識與如 @PreAuthorize("hasAuthority('sys:menu:view')") 標註的接口對比,決定是否能夠調用接口 Set<String> permissions = sysUserService.findPermissions(user.getName()); List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList()); return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities); } }
上面 UserDetailsService 查詢的信息須要封裝到實現 UserDetails 接口的封裝對象裏。
JwtUserDetails.java
package com.louis.kitty.admin.security; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import com.fasterxml.jackson.annotation.JsonIgnore; /** * 安全用戶模型 * @author Louis * @date Nov 20, 2018 */ public class JwtUserDetails implements UserDetails { private static final long serialVersionUID = 1L; private String username; private String password; private String salt; private Collection<? extends GrantedAuthority> authorities; JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) { this.username = username; this.password = password; this.salt = salt; this.authorities = authorities; } @Override public String getUsername() { return username; } @JsonIgnore @Override public String getPassword() { return password; } public String getSalt() { return salt; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public boolean isEnabled() { return true; } }
由於咱們沒有使用內置的 formLogin 登陸處理過濾器,因此須要調用登陸認證流程,修改登陸接口,加入系統登陸認證調用。
SysLoginController.java
/** * 登陸接口 */ @PostMapping(value = "/login") public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException { String username = loginBean.getAccount(); String password = loginBean.getPassword(); String captcha = loginBean.getCaptcha();...
// 系統登陸認證 JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager); return HttpResult.ok(token); }
Spring Security 的登陸認證過程是經過調用 AuthenticationManager 的 authenticate(token) 方法實現的。
登陸流程中主要是返回一個認證好的 Authentication 對象,而後保存到上下文供後續進行受權的時候使用。
登陸認證成功以後,會利用JWT生成 token 返回給客戶端,後續的訪問都須要攜帶此 token 來進行認證。
SecurityUtils.java
/** * 系統登陸認證 * @param request * @param username * @param password * @param authenticationManager * @return */ public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) { JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password); token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 執行登陸認證過程 Authentication authentication = authenticationManager.authenticate(token); // 認證成功存儲認證信息到上下文 SecurityContextHolder.getContext().setAuthentication(authentication); // 生成令牌並返回給客戶端 token.setToken(JwtTokenUtils.generateToken(authentication)); return token; }
令牌生成器主要是利用JWT生成所需的令牌,部分代碼以下。
JwtTokenUtils.java
/** * JWT工具類 * @author Louis * @date Nov 20, 2018 */ public class JwtTokenUtils implements Serializable { /** * 生成令牌 * @param userDetails 用戶 * @return 令牌 */ public static String generateToken(Authentication authentication) { Map<String, Object> claims = new HashMap<>(3); claims.put(USERNAME, SecurityUtils.getUsername(authentication)); claims.put(CREATED, new Date()); claims.put(AUTHORITIES, authentication.getAuthorities()); return generateToken(claims); } /** * 從數據聲明生成令牌 * @param claims 數據聲明 * @return 令牌 */ private static String generateToken(Map<String, Object> claims) { Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME); return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact(); } }
登陸認證過濾器繼承 BasicAuthenticationFilter,在訪問任何URL的時候會被此過濾器攔截,經過調用 SecurityUtils.checkAuthentication(request) 檢查登陸狀態。
JwtAuthenticationFilter.java
package com.louis.kitty.admin.security; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import com.louis.kitty.admin.util.SecurityUtils; /** * 登陸認證過濾器 * @author Louis * @date Nov 20, 2018 */ public class JwtAuthenticationFilter extends BasicAuthenticationFilter { @Autowired public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 獲取token, 並檢查登陸狀態 SecurityUtils.checkAuthentication(request); chain.doFilter(request, response); } }
登陸驗證檢查是經過 SecurityUtils.checkAuthentication(request) 來完成的。
SecurityUtils.java
/** * 獲取令牌進行認證 * @param request */ public static void checkAuthentication(HttpServletRequest request) { // 獲取令牌並根據令牌獲取登陸認證信息 Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request); // 設置登陸認證信息到上下文 SecurityContextHolder.getContext().setAuthentication(authentication); }
上面的登陸驗證是經過 JwtTokenUtils.getAuthenticationeFromToken(request),來驗證令牌並返回登陸信息的。
JwtTokenUtils.java
/** * 根據請求令牌獲取登陸認證信息 * @param token 令牌 * @return 用戶名 */ public static Authentication getAuthenticationeFromToken(HttpServletRequest request) { Authentication authentication = null; // 獲取請求攜帶的令牌 String token = JwtTokenUtils.getToken(request); if(token != null) { // 請求令牌不能爲空 if(SecurityUtils.getAuthentication() == null) { // 上下文中Authentication爲空 Claims claims = getClaimsFromToken(token); if(claims == null) { return null; } String username = claims.getSubject(); if(username == null) { return null; } if(isTokenExpired(token)) { return null; } Object authors = claims.get(AUTHORITIES); List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); if (authors != null && authors instanceof List) { for (Object object : (List) authors) { authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority"))); } } authentication = new JwtAuthenticatioToken(username, null, authorities, token); } else { if(validateToken(token, SecurityUtils.getUsername())) { // 若是上下文中Authentication非空,且請求令牌合法,直接返回當前登陸認證信息 authentication = SecurityUtils.getAuthentication(); } } } return authentication; }
清除掉 config 包下的 ShiroConfig 配置類。
清除 oautho2 包下有關 Shiro 的相關代碼。
清除掉 sys_token 表和相關操做代碼。
後端:https://gitee.com/liuge1988/kitty
前端:https://gitee.com/liuge1988/kitty-ui.git
做者:朝雨憶輕塵
出處:https://www.cnblogs.com/xifengxiaoma/ 版權全部,歡迎轉載,轉載請註明原文做者及出處。