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 authorities; public JwtUser(String username, String password, Collection authorities) { this.username = username; this.password = password; this.authorities = authorities; } @Override public Collection 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. * * @returntrue
if the user is not locked,false
otherwise */ @Override public boolean isAccountNonLocked() { return true; } /** * Indicates whether the user's credentials (password) has expired. Expired * credentials prevent authentication. * * @returntrue
if the user's credentials are valid (ie non-expired), *false
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. * * @returntrue
if the user is enabled,false
otherwise */ @Override public boolean isEnabled() { return true; }
3.JwtUserDetailsServiceImpl
重写security的UserDaiService的loadByusername方法,实现自定义的权限验证
@Servicepublic 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, theUserDetails
* 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 (nevernull
) * @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); ListuserList = 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 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生成方法
@Componentpublic 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(Mapclaims) { 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 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的有效性
@Componentpublic 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. *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 用户验证流程
UsernamePasswordAuthenticationTokenauthenticationManager.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 开源地址
欢迎指导
后续将补上验证流程