基於先後端分離項目的後端模塊;css
實現security的UserDetail。以後全部權限獲取都是從這個對象中返回java
重寫的默認屬性必須返回true,否則在登陸那塊驗證該屬性是否是true。若是默認返回false,會報出各類用戶相關的異常git
@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; }
重寫security的UserDaiService的loadByusername方法,實現自定義的權限驗證github
@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; } }
@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)); } }
每次請求的時候都會被該過濾器過濾攔截。主要是校驗token的有效性web
@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); } }
@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來操做,代碼以下:spring
/** * 登陸操做,返回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); }
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 來加密頁面傳遞過來的密碼信息。而後與數據庫中的密碼比對。若是相同則返回成功,不一樣則報錯
github地址 歡迎指導
後續將補上驗證流程mongodb