springboot、springsecurity、jwt权限验证

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地址 欢迎指导
后续将补上验证流程

posted @ 2018-12-20 10:28  Ronaldo7  阅读(3625)  评论(0编辑  收藏  举报