學習Spring Boot:(二十八)Spring Security 權限認證

前言

主要實現 Spring Security 的安全認證,結合 RESTful API 的風格,使用無狀態的環境。css

主要實現是經過請求的 URL ,經過過濾器來作不一樣的受權策略操做,爲該請求提供某個認證的方法,而後進行認證,受權成功返回受權實例信息,供服務調用。html

基於Token的身份驗證的過程以下:java

  1. 用戶經過用戶名和密碼發送請求。
  2. 程序驗證。
  3. 程序返回一個簽名的token 給客戶端。
  4. 客戶端儲存token,而且每次用於每次發送請求。
  5. 服務端驗證token並返回數據。

每一次請求都須要token,因此每次請求都會去驗證用戶身份,因此這裏必需要使用緩存,mysql

基本使用

加入相關依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

瞭解基礎配置

認證的基本信息
public interface UserDetails extends Serializable {

    //返回分配給用戶的角色列表
    Collection<? extends GrantedAuthority> getAuthorities();

    //返回密碼
    String getPassword();

    //返回賬號
    String getUsername();

    // 帳戶是否未過時
    boolean isAccountNonExpired();

    // 帳戶是否未鎖定
    boolean isAccountNonLocked();

    // 密碼是否未過時
    boolean isCredentialsNonExpired();

    // 帳戶是否激活
    boolean isEnabled();
}
獲取基本信息
// 根據用戶名查找用戶的信息
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

咱們只要實現這個擴展,就可以自定義方式獲取認證的基本信息web

WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapter 提供了一種便利的方式去建立 WebSecurityConfigurer的實例,只須要重寫 WebSecurityConfigurerAdapter 的方法,便可配置攔截什麼URL、設置什麼權限等安全控制。算法

下面是主要會是要到的幾個配置:spring

/** * 主要是對身份認證的設置 * @param auth * @throws Exception */  
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        this.disableLocalConfigureAuthenticationBldr = true;
    }
    /** * 複寫這個方法來配置 {@link HttpSecurity}. * 一般,子類不能經過調用 super 來調用此方法,由於它可能會覆蓋其配置。 默認配置爲: * */
    protected void configure(HttpSecurity http) throws Exception {
        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin().and()
            .httpBasic();
    }

    /** * Override this method to configure {@link WebSecurity}. For example, if you wish to * ignore certain requests. * 主要是對某些 web 靜態資源的設置 */
    public void configure(WebSecurity web) throws Exception {
    }

認證流程

閱讀源碼瞭解。sql

Spring Security.jpg

AbstractAuthenticationProcessingFilter.doFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        // 判斷是不是須要驗證方法(是不是登錄的請求),不是的話直接放過
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        // 登錄的請求開始進行驗證
        Authentication authResult;
        try {
            // 開始認證,attemptAuthentication在 UsernamePasswordAuthenticationFilter 中實現
            authResult = attemptAuthentication(request, response);
            // return null 認證失敗
            if (authResult == null) {
                return;
            }
        // 篇幅問題,中間不少代碼刪了
        successfulAuthentication(request, response, chain, authResult);
    }

UsernamePasswordAuthenticationFilter.attemptAuthentication

// 接收並解析用戶登錄信息,爲已驗證的用戶返回一個已填充的身份驗證令牌,表示成功的身份驗證,
// 若是身份驗證過程失敗,就拋出一個AuthenticationException
public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        // 方法將 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 對象,用於 AuthenticationManager 的驗證
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
        username = username.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

ProviderManager.authenticate

驗證 Authentication 對象(裏面包含着驗證對象)數據庫

  1. 若是有多個 AuthenticationProvider 支持驗證傳遞過來的Authentication 對象,那麼由第一個來肯定結果,覆蓋早期支持AuthenticationProviders 所引起的任何可能的AuthenticationException。 成功驗證後,將不會嘗試後續的AuthenticationProvider。
  2. 若是最後全部的 AuthenticationProviders 都沒有成功驗證 Authentication 對象,將拋出 AuthenticationException。

最後它調用的是 Authentication result = provider.authenticate(authentication);json

只要咱們自定義 AuthenticationProvider 就能完成自定義認證。

動手實現安全框架

使用的依賴

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

數據表關係

img

User
@Data
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false, length = 50)
    private String username;

    @Column(nullable = false)
    private String password;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;

    @OneToMany(targetEntity = UserRole.class, mappedBy = "userId", fetch = FetchType.EAGER) // mappedBy 只有在雙向關聯的時候設置,表示關係維護的一端,不然會生成中間表A_B
    @org.hibernate.annotations.ForeignKey(name = "none") // 注意這裏不能使用 @JoinColumn 否則會生成外鍵
    private Set<UserRole> userRoles;
}
Role
@Entity
@Data
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false, unique = true)
    private String name;
}
UserRole
@Entity
@Data
public class UserRole {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 50, nullable = false)
    private Long userId;

    @ManyToOne(targetEntity = Role.class)
    @JoinColumn(name = "roleId", nullable = false, foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
    private Role role;
}

流程實現

認證流程:
img

JWT

我使用的是服務端無狀態的token 交換的形式,因此引用的是 jwt,首先實現 jwt:

# jwt 配置
jwt:
  # 加密密鑰
  secret: 61D73234C4F93E03074D74D74D1E39D9 #blog.wuwii.com
  # token有效時長
  expire: 7 # 7天,單位天
  # token 存在 header 中的參數
  header: token

@ConfigurationProperties(prefix = "jwt")
@Data
public class JwtUtil {
    /** * 密鑰 */
    private String secret;
    /** * 有效期限 */
    private int expire;
    /** * 存儲 token */
    private String header;

    /** * 生成jwt token * * @param username * @return token */
    public String generateToken(String username) {
        Date nowDate = new Date();

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                // 後續獲取 subject 是 username
                .setSubject(username)
                .setIssuedAt(nowDate)
                .setExpiration(DateUtils.addDays(nowDate, expire))
                // 這裏我採用的是 HS512 算法
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /** * 解析 token, * 利用 jjwt 提供的parser傳入祕鑰, * * @param token token * @return 數據聲明 Map<String, Object> */
    private Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            return null;
        }
    }

    /** * token是否過時 * * @return true:過時 */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }

    public String getUsernameFromToken(String token) {
        if (StringUtils.isBlank(token)) {
            throw new KCException("無效 token", HttpStatus.UNAUTHORIZED.value());
        }
        Claims claims = getClaimByToken(token);
        if (claims == null || isTokenExpired(claims.getExpiration())) {
            throw new KCException(header + "失效,請從新登陸", HttpStatus.UNAUTHORIZED.value());
        }
        return claims.getSubject();
    }
}
實現 UserDetails 和 UserDetailsService

img

實現 UserDetails
public class UserDetailsImpl implements UserDetails {
    private User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    /** * 獲取權限信息 * @return */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<UserRole> userRoles = user.getUserRoles();
        List<GrantedAuthority> auths = new ArrayList<>(userRoles.size());
        userRoles.parallelStream().forEach(userRole -> {
            // 默認ROLE_ 爲前綴,能夠更改
            auths.add(new SimpleGrantedAuthority("ROLE_" + userRole.getRole().getName()));
        });
        return auths;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // 帳戶是否未過時
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 帳戶是否未鎖定
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 密碼是否未過時
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 帳戶是否激活
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}
實現 UserDetailsService
@Slf4j
@CacheConfig(cacheNames = "users")
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserDao userDao;

    @Override
    @Cacheable
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("Username is not valid.");
        }
        log.debug("The User is {}", user);
        return SecurityModelFactory.create(user);
    }
}
SecurityModelFactory

轉換 UserDetails 的工廠類

public class SecurityModelFactory {
    public static UserDetails create(User user) {
        return new UserDetailsImpl(user);
    }
}
受權認證
登錄過濾器
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Autowired
    private JwtUtil jwtUtil;

    /** * 過濾,我目前使用的是默認的,能夠本身看源碼按需求更改 */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // todo 在這裏能夠按需求進行過濾,根據源碼來修改擴展很是方便
        super.doFilter(request, response, chain);
    }

    /** * 若是須要進行登錄認證,會在這裏進行預處理 */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        // todo 在登錄認證的時候,能夠作些其餘的驗證操做,好比驗證碼
        return super.attemptAuthentication(request, response);
    }

    /** * 登錄成功調用,返回 token */
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain, Authentication authResult) throws IOException {
        String token = jwtUtil.generateToken(authResult.getName());
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().print(token);
    }
}
  1. 首先會進入 doFilter 方法中,這裏能夠自定義定義過濾;
  2. 而後若是是登錄的請求,會進入 attemptAuthentication 組裝登錄信息,而且進行登錄認證;
  3. 若是登錄成功,會調用 successfulAuthentication方法。
登錄驗證
@Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    /** * 驗證登陸信息,若登錄成功,設置 Authentication */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (passwordEncoder.matches(password, user.getPassword())) {
            Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
            return new UsernamePasswordAuthenticationToken(username, password, authorities);
        }
        throw new BadCredentialsException("The password is not correct.");
    }

    /** * 當前 Provider 是否支持對該類型的憑證提供認證服務 */
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
    }
}

咱們本身定義的 AuthenticationProvider 主要是實現前面通過過濾器封裝的認證對象 UsernamePasswordAuthenticationToken 進行解析認證,

若是認證成功 就給改 UsernamePasswordAuthenticationToken 設置對應的權限,而後返回 Authentication

  1. 得到認證的信息;
  2. 去數據庫查詢信息,獲取密碼解密驗證認證信息;
  3. 認證成功,設置權限信息,返回 Authentication,失敗拋出異常。
JWT 攔截器
/** * token 校驗 * BasicAuthenticationFilter 濾器負責處理任何具備HTTP請求頭的請求的請求, * 以及一個基本的身份驗證方案和一個base64編碼的用戶名:密碼令牌。 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /** * 在此方法中檢驗客戶端請求頭中的token, * 若是存在併合法,就把token中的信息封裝到 Authentication 類型的對象中, * 最後使用 SecurityContextHolder.getContext().setAuthentication(authentication); 改變或刪除當前已經驗證的 pricipal * * @param request * @param response * @param chain * @throws IOException * @throws ServletException */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

        String token = request.getHeader(jwtUtil.getHeader());

        //判斷是否有token
        if (token == null) {
            chain.doFilter(request, response);
            return;
        }
        // 經過token 獲取帳戶信息,而且存入到將身份信息存放在安全系統的上下文。
        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(token);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        chain.doFilter(request, response);
    }

    /** * 解析token中的信息 */
    private UsernamePasswordAuthenticationToken getAuthentication(String token) {
        String username = jwtUtil.getUsernameFromToken(token);
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (username != null) {
            return new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
        }
        return null;
    }
}
  1. 請求進入 doFilterInternal 方法中,對請求是否帶token進行判斷,
  2. 若是沒有token,則直接放行請求;
  3. 若是有 token,則解析它的 post;
配置權限和相關設置

自定義配置 Spring Security 配置類 WebSecurityConfig,進項相關配置,而且將所須要的類注入到系統中。

@Configuration
@EnableWebSecurity // 開啓 Security
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
//jsr250Enabled有三種註解,分別是@RolesAllowed,@PermitAll,@DenyAll,功能跟名字同樣,
// securedEnabled 開啓註解
// prePostEnabled 相似用的最多的是 @PreAuthorize
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public JwtUtil jwtUtil() {
        return new JwtUtil();
    }

    /** * 注入 LoginFilter 時候須要,注入 authenticationManager */
    @Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setAuthenticationManager(authenticationManager());
        return loginFilter;
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        return new JwtAuthenticationFilter(authenticationManager());
    }
    @Bean
    public UserDetailsService customService() {
        return new UserDetailServiceImpl();
    }

    /** * 認證 AuthenticationProvider */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        return new CustomAuthenticationProvider();
    }

    /** * BCrypt算法免除存儲salt * BCrypt算法將salt隨機並混入最終加密後的密碼,驗證時也無需單獨提供以前的salt,從而無需單獨處理salt問題。 * @return */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(5);
    }

    /** * 主要是對身份驗證的設置 */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth
                // 注入身份的 Bean
                .authenticationProvider(authenticationProvider())
                .userDetailsService(userDetailsService())
                // 默認登錄的加密,自定義登錄的時候無效
                .passwordEncoder(passwordEncoder());
        // 在內存中設置固定的帳戶密碼以及身份信息
        /*auth .inMemoryAuthentication().withUser("user").password("password").roles("USER").and() .withUser("admin").password("password").roles("USER", "ADMIN");*/
    }

    /** * * @param http * @throws Exception */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 關閉 csrf
                .csrf().disable()
                // 設置 session 狀態 STATELESS 無狀態
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 須要權限驗證
                .mvcMatchers("/user/**").authenticated()
                .and()
                // 登錄頁面
                .formLogin()
                //.loginPage("/login.html")
                // 登錄成功跳轉頁面
                .defaultSuccessUrl("/")
                //.failureForwardUrl("/login.html")
                .permitAll()
                .and()
                // 登出
                //.logout()
                // 註銷的時候刪除會話
                //.deleteCookies("JSESSIONID")
                // 默認登出請求爲 /logout,能夠用下面自定義
                //.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                // 自定義登出成功的頁面,默認爲登錄頁
                //.logoutSuccessUrl("/logout.html")
                //.permitAll()
                //.and()
                // 開啓 cookie 保存用戶信息
                //.rememberMe()
                // cookie 有效時間
                //.tokenValiditySeconds(60 * 60 * 24 * 7)
                // 設置cookie 的私鑰,默認爲隨機生成的key
                //.key("remember")
                //.and()
                //驗證登錄的 filter
                .addFilter(loginFilter())
                //驗證token的 filter
                .addFilter(jwtAuthenticationFilter());
    }

    /** * Web層面的配置,通常用來配置無需安全檢查的路徑 * @param web * @throws Exception */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web
                .ignoring()
                .antMatchers(
                        "**.js",
                        "**.css",
                        "/images/**",
                        "/webjars/**",
                        "/**/favicon.ico"
                );
    }
}
權限控制
@RestController
@RequestMapping(value = "/user", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@PreAuthorize("hasRole('USER')")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping
    @PreAuthorize("hasRole('admin')")
    public ResponseEntity<List<UserVO>> getAllUser() {
        List<User> users = userService.findAll();
        List<UserVO> userViews = userService.castUserVO(users);
        return ResponseEntity.ok(userViews);
    }
}

請求上面的getAllUser 方法,須要當前用戶同時擁有 ROLE_USERROLE_admin 兩個權限,才能經過權限驗證。

在 @PreAuthorize 中咱們能夠利用內建的 SPEL 表達式:好比 ‘hasRole()’ 來決定哪些用戶有權訪問。需注意的一點是 hasRole 表達式認爲每一個角色名字前都有一個前綴 ‘ROLE_’。

迭代上個版本

後來,我發現進行用戶認證的時候,會將全部的 provider 都嘗試一遍,那麼外面將登錄的 UsernameAndPasswordTokenJwtTToken 均可以分別進行驗證進行了啊,全部我預先定義 UsernamePasswordAuthenticationToken 包裝登錄的信息,而後進入登錄的 AuthenticationProvider 進行認證,token 驗證形式,使用 PreAuthenticatedAuthenticationToken 的包裝,而後進入例外一個 `AuthenticationProvider ` 中認證。

如今咱們的流程就更加清晰了。

img

因此如今我對之前的權限配置以及認證進行了一些更改:

過濾器

在這裏,我根據不一樣請求的類型,進行不一樣的適配,而後進行加工分裝成不一樣的認證憑證,而後根據憑證的不一樣,進行不一樣的認證。

@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        try {
            if (isLoginRequest(httpRequest, httpResponse)) {
                Authentication authResult = processLogin(httpRequest, httpResponse);
                successfulAuthentication(httpRequest, httpResponse, chain, authResult);
                return;
            }
            String token = obtainToken(httpRequest);
            if (StringUtils.isNotBlank(token)) {
                processTokenAuthentication(token);
            }
        } catch (AuthenticationException e) {
            unsuccessfulAuthentication(httpRequest, httpResponse, e);
            return;
        }
        chain.doFilter(request, response);
    }
    /** * 登錄成功調用,返回 token */
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain, Authentication authResult) throws IOException {
        String token = jwtUtil.generateToken(authResult.getName());
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().print(token);
    }

    private boolean isLoginRequest(HttpServletRequest request, HttpServletResponse response) {
        return requiresAuthentication(request, response) && "POST".equalsIgnoreCase(request.getMethod());
    }

    private String obtainToken(HttpServletRequest request) {
        return request.getHeader(jwtUtil.getHeader());
    }

    private Authentication processLogin(HttpServletRequest request, HttpServletResponse response) {
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        return tryAuthenticationWithUsernameAndPassword(username, password);
    }

    private void processTokenAuthentication(String token) {
        Authentication resultOfAuthentication = tryToAuthenticateWithToken(token);
        // 設置上下文用戶信息以及權限
        SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
    }

    private Authentication tryAuthenticationWithUsernameAndPassword(String username, String password) {
        Authentication authentication = new UsernamePasswordAuthenticationToken(username, password);
        return tryToAuthenticate(authentication);
    }

    private Authentication tryToAuthenticateWithToken(String token) {
        PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token, null);
        return tryToAuthenticate(requestAuthentication);
    }

    private Authentication tryToAuthenticate(Authentication requestAuth) {
        Authentication responseAuth = getAuthenticationManager().authenticate(requestAuth);
        if (responseAuth == null || !responseAuth.isAuthenticated()) {
            throw new InternalAuthenticationServiceException("Unable to authenticate User for provided credentials");
        }
        log.debug("User successfully authenticated");
        return responseAuth;
    }
}

受權認證

根據提供的憑證的類型,進行相關的驗證操做

LoginAuthenticationProvider

跟上個版本的 登錄驗證中的 CustomAuthenticationProvider 代碼同樣實現同樣。

TokenAuthenticateProvider

根據 token 查找它的 權限 信息,並裝在到認證的憑證中。

public class TokenAuthenticateProvider implements AuthenticationProvider {
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String token = authentication.getName();
        String username = jwtUtil.getUsernameFromToken(token);
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new PreAuthenticatedAuthenticationToken(username, null, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return PreAuthenticatedAuthenticationToken.class.equals(authentication);
    }
}

配置權限和相關設置

和上個版本沒什麼變化,只是將類換了一下

@Configuration
@EnableWebSecurity // 開啓 Security
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public JwtUtil jwtUtil() {
        return new JwtUtil();
    }

    @Bean
    public UserDetailsService customService() {
        return new UserDetailServiceImpl();
    }

    @Bean("loginAuthenticationProvider")
    public AuthenticationProvider loginAuthenticationProvider() {
        return new LoginAuthenticationProvider();
    }

    @Bean("tokenAuthenticationProvider")
    public AuthenticationProvider tokenAuthenticationProvider() {
        return new TokenAuthenticateProvider();
    }

    @Bean
    public AuthenticationFilter authenticationFilter() throws Exception {
        AuthenticationFilter authenticationFilter = new AuthenticationFilter();
        authenticationFilter.setAuthenticationManager(authenticationManager());
        return authenticationFilter;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(5);
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        return new UserDetailServiceImpl();
    }
    /** * 主要是對身份驗證的設置 */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .authenticationProvider(loginAuthenticationProvider())
                .authenticationProvider(tokenAuthenticationProvider())
                .userDetailsService(userDetailsService());

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 關閉 csrf
                .csrf().disable()
                // 設置 session 狀態 STATELESS 無狀態
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 須要權限驗證
                .mvcMatchers("/user/**").authenticated()
                .and()
                // 登錄頁面
                .formLogin()
                //.loginPage("/login.html")
                // 登錄成功跳轉頁面
                .defaultSuccessUrl("/")
                .failureForwardUrl("/login.html")
                .permitAll()
                .and()
                .addFilter(authenticationFilter())
        ;
    }
}

後續完善

  1. 修改密碼,登出操做 token 的失效機制;
  2. OAuth2 受權服務器的搭建;
  3. 修改權限後,下次請求刷新權限;
  4. ……

附錄一:HttpSecurity經常使用方法

方法 說明
openidLogin() 用於基於 OpenId 的驗證
headers() 將安全標頭添加到響應
cors() 配置跨域資源共享( CORS )
sessionManagement() 容許配置會話管理
portMapper() 容許配置一個PortMapper(HttpSecurity#(getSharedObject(class))),其餘提供SecurityConfigurer的對象使用 PortMapper 從 HTTP 重定向到 HTTPS 或者從 HTTPS 重定向到 HTTP。默認狀況下,Spring Security使用一個PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee() 配置基於容器的預認證。 在這種狀況下,認證由Servlet容器管理
x509() 配置基於x509的認證
rememberMe 容許配置「記住我」的驗證
authorizeRequests() 容許基於使用HttpServletRequest限制訪問
requestCache() 容許配置請求緩存
exceptionHandling() 容許配置錯誤處理
securityContext() HttpServletRequests之間的SecurityContextHolder上設置SecurityContext的管理。 當使用WebSecurityConfigurerAdapter時,這將自動應用
servletApi() HttpServletRequest方法與在其上找到的值集成到SecurityContext中。 當使用WebSecurityConfigurerAdapter時,這將自動應用
csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter時,默認啓用
logout() 添加退出登陸支持。當使用WebSecurityConfigurerAdapter時,這將自動應用。默認狀況是,訪問URL」/ logout」,使HTTP Session無效來清除用戶,清除已配置的任何#rememberMe()身份驗證,清除SecurityContextHolder,而後重定向到」/login?success」
anonymous() 容許配置匿名用戶的表示方法。 當與WebSecurityConfigurerAdapter結合使用時,這將自動應用。 默認狀況下,匿名用戶將使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,幷包含角色 「ROLE_ANONYMOUS」
formLogin() 指定支持基於表單的身份驗證。若是未指定FormLoginConfigurer#loginPage(String),則將生成默認登陸頁面
oauth2Login() 根據外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份驗證
requiresChannel() 配置通道安全。爲了使該配置有用,必須提供至少一個到所需信道的映射
httpBasic() 配置 Http Basic 驗證
addFilterAt() 在指定的Filter類的位置添加過濾器
相關文章
相關標籤/搜索