springboot、springsecurity、jwt權限驗證

1.背景

基於先後端分離項目的後端模塊;css

2.相關技術

  • springboot全家桶
    • web模塊
    • security模塊;用於權限的驗證
    • mongodb 模塊;集成mogodb模塊
  • jwt 用於token的生成
  • mongodb
  • lomok
  • 後續會細分出更多的模塊。用上springcloud全家桶

3.權限驗證流程

3.1 構建User對象

實現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;
    }

3.JwtUserDetailsServiceImpl

重寫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;
    }
}

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的有效性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);
    }
}

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來操做,代碼以下: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);
    }

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地址 歡迎指導
後續將補上驗證流程mongodb

相關文章
相關標籤/搜索