Spring Boot 中使用 Spring Security + JWT 構(gòu)建身份認(rèn)證系統(tǒng)

權(quán)限控制是非常常見的功能粥喜,在各種后臺(tái)管理里權(quán)限控制更是重中之重。在 Spring Boot 中使用 Spring Security 構(gòu)建權(quán)限系統(tǒng)是非常輕松和簡(jiǎn)單的击纬。Spring Security 是一個(gè)能夠?yàn)榛?Spring 的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架虐呻。它提供了一組可以在 Spring 應(yīng)用上下文中配置的 Bean绊困,為應(yīng)用系統(tǒng)提供聲明式的安全訪問控制功能去团,減少了為企業(yè)系統(tǒng)安全控制編寫大量重復(fù)代碼的工作抡诞。

添加依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

UserDetails

按照官方文檔的說(shuō)法,為了定義我們自己的認(rèn)證管理土陪,我們可以添加 UserDetailsService, AuthenticationProvider, AuthenticationManager 這種類型的 Bean昼汗。實(shí)現(xiàn)的方式有多種,這里我選擇最簡(jiǎn)單的一種(因?yàn)楸旧砦覀冞@里的認(rèn)證授權(quán)也比較簡(jiǎn)單)通過(guò)定義自己的 UserDetailsService 從數(shù)據(jù)庫(kù)查詢用戶信息鬼雀,至于認(rèn)證的話就用默認(rèn)的顷窒。

/**
 * Author: vincent
 * Date: 2018-06-12 16:50:00
 * Comment:
 */

public class User implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    private Date lastPasswordResetDate;
    private List<Role> roles = new ArrayList<>();

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @JsonIgnore
    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream().map(Role::getName).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return true;
    }

    @JsonIgnore
    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }

    public void setLastPasswordResetDate(Date lastPasswordResetDate) {
        this.lastPasswordResetDate = lastPasswordResetDate;
    }
}

開啟權(quán)限驗(yàn)證

/**
 * Author: vincent
 * Date: 2018-06-12 17:02:00
 * Comment:
 * @EnableWebSecurity 開啟權(quán)限驗(yàn)證注解
 * @EnableGlobalMethodSecurity(prePostEnabled = true) 開啟方法級(jí)別的權(quán)限驗(yàn)證
 */

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 因?yàn)槲覀儗?shí)現(xiàn)了 UserDetailsService,Spring 會(huì)自動(dòng)在項(xiàng)目中查找它的實(shí)現(xiàn)類取刃,
     * 這里注入的也自然是我們自定義的 UserDetailsServiceImpl 類
     */
    @Resource
    private UserDetailsService userDetailsService;

    @Resource
    private AuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 設(shè)置我們自己實(shí)現(xiàn)的的 UserDetailsService蹋肮,并設(shè)置密碼的加密方式,加密方式不是必須的璧疗,也就是說(shuō),這里是可以明文密碼
     * @param authenticationManagerBuilder
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 因?yàn)槲覀兪腔?token 進(jìn)行驗(yàn)證的馁龟,所以 csrf 和 session 我們這里都不需要
        httpSecurity.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()

                // 獲取 token 的接口所有人都可以訪問
                .antMatchers("/authentication/**").permitAll()

                // 除了上面的接口崩侠,其它接口都必須鑒權(quán)認(rèn)證
                .anyRequest().authenticated();
        httpSecurity.headers().cacheControl();

        // 將我們實(shí)現(xiàn)的過(guò)濾器添加進(jìn)去,方法名可以很輕松的看出來(lái)是前置過(guò)濾
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

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

集成 JWT

想要將 JWT(Json Web Token) 在 spring boot 中集成起來(lái)坷檩,需要?jiǎng)?chuàng)建一個(gè) filter却音,繼承自 OncePerRequestFilter改抡。
并且將它添加到 WebSecurityConfig,需要注意的是系瓢,這個(gè)過(guò)濾器是作為權(quán)限驗(yàn)證阿纤,必須添加到 before 集合中。

/**
 * Author: vincent
 * Date: 2018-06-12 17:13:00
 * Comment:
 */

@Component
public class AuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private UserDetailsService userDetailsService;

    @Resource
    private TokenUtil tokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorization = request.getHeader(AUTHORIZATION);
        if (!StringUtils.isEmpty(authorization) && authorization.startsWith(TokenUtil.TOKEN_PREFIX)) {
            String token = authorization.substring(TokenUtil.TOKEN_PREFIX.length());
            String username = tokenUtil.getUsernameFromToken(token);
            if (!StringUtils.isEmpty(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                if (tokenUtil.validateToken(token, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities()
                    );
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}
/**
 * Author: vincent
 * Date: 2018-06-12 17:17:00
 * Comment:
 */

@Component
public class TokenUtil implements Serializable {
    public static final String TOKEN_PREFIX = "Bearer ";

    static final String CLAIM_KEY_USERNAME = "sub";
    static final String CLAIM_KEY_CREATED = "iat";

    private static final long serialVersionUID = -3301605591108950415L;

    private Clock clock = DefaultClock.INSTANCE;

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getIssuedAtDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getIssuedAt);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(clock.now());
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    private Boolean ignoreTokenExpiration(String token) {
        return false;
    }

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }

    private String doGenerateToken(Map<String, Object> claims, String subject) {
        final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
        final Date created = getIssuedAtDateFromToken(token);
        return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
                && (!isTokenExpired(token) || ignoreTokenExpiration(token));
    }

    public String refreshToken(String token) {
        final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);

        final Claims claims = getAllClaimsFromToken(token);
        claims.setIssuedAt(createdDate);
        claims.setExpiration(expirationDate);

        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        if (userDetails instanceof User) {
            User user = (User) userDetails;
            final String username = getUsernameFromToken(token);
            final Date created = getIssuedAtDateFromToken(token);
            return (
                    username.equals(user.getUsername())
                            && !isTokenExpired(token)
                            && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
            );
        } else {
            System.out.println("類型轉(zhuǎn)換失斠穆欠拾!");
            return false;
        }
    }

    private Date calculateExpirationDate(Date createdDate) {
        return new Date(createdDate.getTime() + expiration * 1000);
    }
}

UserDetailsService

/**
 * Author: vincent
 * Date: 2018-06-12 16:49:00
 * Comment: 這個(gè)接口只定義了一個(gè)方法 loadUserByUsername,顧名思義骗绕,就是提供一種從用戶名可以查到用戶并返回的方法藐窄。
 * 注意,不一定是數(shù)據(jù)庫(kù)酬土,文本文件荆忍、xml文件等等都可能成為數(shù)據(jù)源,這也是為什么 Spring 提供這樣一個(gè)接口的原因:保證你可以采用靈活的數(shù)據(jù)源撤缴。
 */

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    @Resource
    private RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.findByUsername(username);
        if (user == null)
            throw new UsernameNotFoundException("Cannot find user with username, username = " + username);
        user.setRoles(roleMapper.selectByUserId(user.getId()));
        return user;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末刹枉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子屈呕,更是在濱河造成了極大的恐慌嘶卧,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凉袱,死亡現(xiàn)場(chǎng)離奇詭異芥吟,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)专甩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門钟鸵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人涤躲,你說(shuō)我怎么就攤上這事棺耍。” “怎么了种樱?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵蒙袍,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我嫩挤,道長(zhǎng)害幅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任岂昭,我火速辦了婚禮以现,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己邑遏,他們只是感情好佣赖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著记盒,像睡著了一般憎蛤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纪吮,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天俩檬,我揣著相機(jī)與錄音,去河邊找鬼彬碱。 笑死豆胸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的巷疼。 我是一名探鬼主播晚胡,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嚼沿!你這毒婦竟也來(lái)了估盘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤骡尽,失蹤者是張志新(化名)和其女友劉穎遣妥,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攀细,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡箫踩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谭贪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片境钟。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖俭识,靈堂內(nèi)的尸體忽然破棺而出慨削,到底是詐尸還是另有隱情,我是刑警寧澤套媚,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布缚态,位于F島的核電站,受9級(jí)特大地震影響堤瘤,放射性物質(zhì)發(fā)生泄漏玫芦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一宙橱、第九天 我趴在偏房一處隱蔽的房頂上張望姨俩。 院中可真熱鬧蘸拔,春花似錦师郑、人聲如沸环葵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)张遭。三九已至,卻和暖如春地梨,著一層夾襖步出監(jiān)牢的瞬間菊卷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工宝剖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留洁闰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓万细,卻偏偏與公主長(zhǎng)得像扑眉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赖钞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容