1.背景
基於前后端分離項目的后端模塊;
2.相關技術
- springboot全家桶
- web模塊
- security模塊;用於權限的驗證
- mongodb 模塊;集成mogodb模塊
- jwt 用於token的生成
- mongodb
- lomok
- 后續會細分出更多的模塊。用上springcloud全家桶
3.權限驗證流程
3.1 構建User對象
實現security的UserDetail。之后所有權限獲取都是從這個對象中返回
重寫的默認屬性必須返回true,不然在登錄那塊驗證該屬性是不是true。如果默認返回false,會報出各種用戶相關的異常
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JwtUser implements UserDetails {
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
*
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
*
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
*
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
*/
@Override
public boolean isEnabled() {
return true;
}
3.JwtUserDetailsServiceImpl
重寫security的UserDaiService的loadByusername方法,實現自定義的權限驗證
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
//設置查詢條件,郵箱是唯一的
User queryUser = new User();
queryUser.setEmail(username);
List<User> userList = null;
try {
userList = this.userService.getUser(queryUser);
if (CollectionUtils.isEmpty(userList)) {
//return new JwtUser(username, queryUser.getPwd(), authorities);
throw new UsernameNotFoundException("用戶賬號:" + username + ",不存在");
} else {
queryUser = userList.get(0);
Set<GrantedAuthority> authorities = new HashSet<>();
//獲取該用戶所有的權限信息
this.userService.getRoleByUserId(queryUser.getId()).forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role.getRoleCode()));
});
return new JwtUser(username, queryUser.getPwd(), authorities);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
3.3 token生成方法
@Component
public class JwtTokenUtil implements Serializable {
/**
* 密鑰
*/
private final String secret = "code4fun";
final static Long TIMESTAMP = 86400000L;
final static String TOKEN_PREFIX = "Bearer";
/**
* 從數據聲明生成令牌
*
* @param claims 數據聲明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + TIMESTAMP);
return TOKEN_PREFIX + " " +Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 從令牌中獲取數據聲明
*
* @param token 令牌
* @return 數據聲明
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
*
* @param userDetails 用戶
* @return 令牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* 從令牌中獲取用戶名
*
* @param token 令牌
* @return 用戶名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判斷令牌是否過期
*
* @param token 令牌
* @return 是否過期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 驗證令牌
*
* @param token 令牌
* @param userDetails 用戶
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
3.4 token校驗過濾器
每次請求的時候都會被該過濾器過濾攔截。主要是校驗token的有效性
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUserDetailsServiceImpl userDetailsService;
private JwtTokenUtil jwtTokenUtil;
public JwtAuthenticationTokenFilter(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
/**
* 每個請求都被攔截
* Same contract as for {@code doFilter}, but guaranteed to be
* just invoked once per request within a single request thread.
* See {@link #shouldNotFilterAsyncDispatch()} for details.
* <p>Provides HttpServletRequest and HttpServletResponse arguments instead of the
* default ServletRequest and ServletResponse ones.
*
* @param request
* @param response
* @param filterChain
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String tokenHead = "Bearer ";
if (authHeader != null && authHeader.startsWith(tokenHead)) {
String authToken = authHeader.substring(tokenHead.length());
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//返回jwtUser
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
//將該用戶的權限信息存放到threadlocal中
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
3.4 webSecurity配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtUserDetailsServiceImpl userDetailsService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
// private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
// private RestAccessDeniedHandler restAccessDeniedHandler;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 注入密碼BCryptPasswordEncoder
* 在添加用戶的時候,要用 BCryptPasswordEncoder.encode()加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/user/**", "/login",
"/js/**", "/bootstrap/**", "/css/**", "/images/**", "/fonts/**").permitAll() //靜態文件攔截
.anyRequest().authenticated()
.and().headers().cacheControl();
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
至此,相關的配置就配置完了。在登錄操作的時候需要注意一下:
用戶信息的驗證全部交給spring security來操作,代碼如下:
/**
* 登錄操作,返回token
* @param userName
* @param password
* @return
* @throws Exception
*/
@Override
public String login(String userName, String password) throws Exception {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(userName, password);
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
return jwtTokenUtil.generateToken(userDetails);
}
3.4 用戶驗證流程
UsernamePasswordAuthenticationToken
authenticationManager.authenticate(upToken);
//通過這個創建一個代理(ProviderManager)對象
delegate = this.delegateBuilder.getObject();
//調用代理對象的認證方法
delegate.authenticate(authentication)
1.代理對象調用父類的 parent.authenticate(authentication);認證方法
1.進到parent.authenticate方法,去定ProvideManager的具體類型是DaoProviderManager
2.provider.authenticate(authentication); //此時的provider是DaoProviderManager
1.判斷參數authentication是不是UsernamePasswordAuthenticationToken類型;不是則跑出異常
2.取出唯一標識字段username
1.判斷userCache是否包含user緩存
1.不在緩存中,創建user對象並存放到緩存中
//調用這個方法轉換成user對象
1.user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
//調用用戶自定義實現了UserDetailService的方法來獲得user對象
1.UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
2.preAuthenticationChecks.check(user);
1.preAuthenticationChecks.check校驗上一部返回的user對象的屬性,只要用戶實現的userDetail的get,set方法賦上值就好了
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
1.uthentication.getCredentials() == null判斷密碼是不是為空
2.presentedPassword = authentication.getCredentials().toString(); 獲取頁面傳遞過來的密碼
3.passwordEncoder.matches(presentedPassword, userDetails.getPassword())判斷頁面上傳遞過來的密碼跟數據庫中的密碼是不是一致。
1.調用BCrypt.checkpw(rawPassword.toString(), encodedPassword)比對
1.調用 hashpw 來加密頁面傳遞過來的密碼信息。然后與數據庫中的密碼比對。如果相同則返回成功,不同則報錯
3.5 開源地址
github地址 歡迎指導
后續將補上驗證流程