權(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;
}
}