SpringSecurity是什么
Spring Security是一個(gè)能夠?yàn)榛赟pring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應(yīng)用上下文中配置的Bean颜懊,充分利用了Spring IoC财岔,DI(控制反轉(zhuǎn)Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,為應(yīng)用系統(tǒng)提供聲明式的安全訪問控制功能河爹,減少了為企業(yè)系統(tǒng)安全控制編寫大量重復(fù)代碼的工作匠璧。(引用百度百科)
為什么使用SpringSecurity
可以說SpringBoot的快速發(fā)展也使得了SpringSecurity的熱度往上漲,在SpringBoot以前咸这,Shiro和SpringSecurity的框架都是主流的安全框架夷恍,但Security的配置非常臃腫,性能也相對(duì)慢一些媳维,最大的缺點(diǎn)還是必須依賴于Spring框架才能開發(fā)酿雪,不過唯一值得一提的就是Security默認(rèn)實(shí)現(xiàn)了更多功能,更是提供了oauth授權(quán)的實(shí)現(xiàn)侄刽。不過一切的改變還是源于SpringBoot的出現(xiàn)指黎,SpringBoot整合SpringSecurity的步驟非常簡(jiǎn)單,只需要繼承WebSecurityConfigurerAdapter這個(gè)類并實(shí)現(xiàn)認(rèn)證方法州丹,接著配置一下登錄的uri即可完成一個(gè)簡(jiǎn)單的用戶認(rèn)證功能醋安,可以說整個(gè)功能都不用5分鐘就能實(shí)現(xiàn)。我個(gè)人還是比較喜歡SpringBoot全家桶墓毒,不需要解決框架整合上的小麻煩吓揪,功能上來說也很強(qiáng)大。
JWT
JWT是一種用于雙方之間傳遞安全信息的簡(jiǎn)潔的所计、URL安全的表述性聲明規(guī)范磺芭。JWT作為一個(gè)開放的標(biāo)準(zhǔn)(RFC 7519),定義了一種簡(jiǎn)潔的醉箕,自包含的方法用于通信雙方之間以Json對(duì)象的形式安全的傳遞信息钾腺。因?yàn)閿?shù)字簽名的存在徙垫,這些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘鑰對(duì)進(jìn)行簽名放棒。簡(jiǎn)潔(Compact): 可以通過URL姻报,POST參數(shù)或者在HTTP header發(fā)送,因?yàn)閿?shù)據(jù)量小间螟,傳輸速度也很快 自包含(Self-contained):負(fù)載中包含了所有用戶所需要的信息吴旋,避免了多次查詢數(shù)據(jù)庫
怎么使用
使用JWT,我們只需要在請(qǐng)求的請(qǐng)求頭上添加如圖下類似的數(shù)據(jù)(token)厢破。后端根據(jù)需要認(rèn)證的url進(jìn)行攔截荣瑟,取出Hearders里面的數(shù)據(jù),緊接著解析出這段token的包含的信息摩泪,判斷信息是否正確即可笆焰。token其實(shí)就是根據(jù)信息加密而來的一段字符串,我們將需要用到的信息放到token中见坑,token包含的信息盡可能的簡(jiǎn)潔嚷掠。
注意:
雖然簡(jiǎn)單的jwt認(rèn)證并沒有什么難度,但如果你沒使用過SpringSecurity荞驴,建議還是先去簡(jiǎn)單的學(xué)習(xí)一下不皆。
開始
- 編寫通過用戶id或用戶手機(jī)號(hào)碼查詢User和Role的方法
- 編寫Token生成工具類
- 繼承UserDetails接口
- 繼承UserDetailsService接口,實(shí)現(xiàn)用戶認(rèn)證方法
- 編寫用戶賬號(hào)驗(yàn)證失敗處理器與權(quán)限不足處理器
- 編寫Token驗(yàn)證過濾器
- 配置SpringSecurity Config
- 實(shí)現(xiàn)登錄方法
整個(gè)流程還是相對(duì)完善的熊楼,所以步驟稍多
導(dǎo)入jar
這里需要提一下的就是霹娄,當(dāng)你引入這個(gè)包的時(shí)候,SpringBoot默認(rèn)會(huì)為項(xiàng)目所有的請(qǐng)求添加認(rèn)證鲫骗,這也是SpringSecurity的常規(guī)操作犬耻,如果你還不知道的話,趕快剎車調(diào)頭回家補(bǔ)課挎峦。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
實(shí)現(xiàn)用戶登錄方法
用戶通過手機(jī)號(hào)及密碼進(jìn)行登錄,我們需要先獲取用戶的身份信息以及角色信息
UserMapper.xml
<resultMap id="User_Role" type="com.viu.technology.po.User">
<id property="id" column="id" javaType="java.lang.String" jdbcType="BIGINT" />
<result property="name" column="name" javaType="java.lang.String" jdbcType="VARCHAR" />
<result property="phone" column="phone" javaType="java.lang.String" jdbcType="VARCHAR" />
<result property="password" column="password" javaType="java.lang.String" jdbcType="VARCHAR" />
<collection property="roles" ofType="com.viu.technology.po.Role">
<id property="id" column="role_id" jdbcType="BIGINT"/>
<result property="roleName" column="role_name" jdbcType="VARCHAR" />
</collection>
</resultMap>
<select id="selUserAndRoleByPhone" parameterType="java.lang.String" resultMap="User_Role">
SELECT u.id,u.name,u.password,u.phone,r.id as role_id ,r.role_name
from t_user u
LEFT JOIN t_role r on u.id=r.user_id
where u.phone=#{phone,jdbcType=VARCHAR}
</select>
<select id="selUserAndRoleById" parameterType="java.lang.String" resultMap="User_Role">
SELECT u.id,u.name,u.password,u.phone,r.id as role_id ,r.role_name
from t_user u
LEFT JOIN t_role r on u.id=r.user_id
where u.id=#{id,jdbcType=VARCHAR}
</select>
UserMapper.java
User selUserAndRoleByPhone(String phone);
User selUserAndRoleById(String id);
UserDao.java
User selUserAndRoleByPhone(String phone);
User selUserAndRoleById(String id);
UserDaoImpl.java
public User selUserAndRoleByPhone(String phone) {
User user = userMapper.selUserAndRoleByPhone(phone);
return user;
}
public User selUserAndRoleById(String id){
User user = userMapper.selUserAndRoleById(id);
return user;
}
UserService.java
User getUserAndRoleByPhone(String phone);
User getUserAndRoleById(String id);
UserServiceImpl.java
public User getUserAndRoleByPhone(String phone) {
User user = userDao.selUserAndRoleByPhone(phone);
return user;
}
public User getUserAndRoleById(String id) {
User user = userDao.selUserAndRoleById(id);
return user;
}
操作數(shù)據(jù)庫獲取用戶身份信息的代碼就到此為止了合瓢,接下來就開始編寫SpringSecurity+jwt的認(rèn)證代碼了
編寫Token生成工具類----JwtTokenUtil
工具類主要用作生成token坦胶、刷新token以及驗(yàn)證token。Token和Session一個(gè)很大的區(qū)別就是無登錄狀態(tài)晴楔,我們可以利用清除session做登出的操作顿苇,但無法利用token直接做登出操作,后續(xù)會(huì)進(jìn)行講解税弃。
這個(gè)token里的信息比較簡(jiǎn)單纪岁,只存放了sub和create,你可以根據(jù)自己業(yè)務(wù)需求在generateToken(UserDetails userDetails)方法里面添加不同的數(shù)據(jù)即可则果,后續(xù)通過getClaimsFromToken方法獲取Claims對(duì)象幔翰,接著調(diào)用Claims對(duì)象的get方法獲取出對(duì)應(yīng)的數(shù)據(jù)即可漩氨。
@Component
public class JwtTokenUtil{
/**
* 密鑰
*/
private static final String secret = "lkhouhubkljgpihojblkjboiboihu9u";
/**
* 從數(shù)據(jù)聲明生成令牌
*
* @param claims 數(shù)據(jù)聲明
* @return 令牌
*/
public static String generateToken(Map<String, Object> claims) {
//設(shè)置token的有效期為24*7小時(shí),也就是一周
Date expirationDate = new Date(System.currentTimeMillis() +60*60*24*7 * 1000);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 從令牌中獲取數(shù)據(jù)聲明
*
* @param token 令牌
* @return 數(shù)據(jù)聲明
*/
public static 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 static 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 static 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 static 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 static 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;
}
/**
* 驗(yàn)證令牌
*
* @param token 令牌
* @param userDetails 用戶
* @return 是否有效
*/
public static Boolean validateToken(String token, UserDetails userDetails) {
String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
繼承UserDetails接口
UserDetails接口是SpringSecurity框架用于認(rèn)證授權(quán)的一個(gè)載體,只有實(shí)現(xiàn)了這個(gè)接口的類才能被SpringSecurity驗(yàn)證遗增,
public class User implements UserDetails {
private String id;
private String name;
private String password;
private String phone;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public User(String id, String name, String password, String phone) {
this.id = id;
this.name = name;
this.password = password;
this.phone = phone;
}
public User() {
super();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//獲取用戶角色權(quán)限叫惊,此處從數(shù)據(jù)庫表Role中獲取
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auths = new ArrayList<>();
List<Role> roles = getRoles();
if (roles!=null) {
for (Role role : roles) {
auths.add(new SimpleGrantedAuthority(role.getRoleName()));
}
}
return auths;
}
//這個(gè)是UserDetails默認(rèn)實(shí)現(xiàn)獲取密碼的方法
@Override
public String getPassword() {
return password;
}
//這里getUsername翻譯過來就是獲取用戶名的意思,但這個(gè)可以作為我們獲取用戶信息的一個(gè)標(biāo)識(shí)
@Override
public String getUsername() {
return id;
}
//用戶賬號(hào)是否過期做修,暫時(shí)沒這個(gè)功能霍狰,默認(rèn)返回true,即未過期
@Override
public boolean isAccountNonExpired() {
return true;
}
//用戶賬號(hào)是否鎖定
@Override
public boolean isAccountNonLocked() {
return true;
}
//用戶憑證是否過期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//賬號(hào)是否可用
@Override
public boolean isEnabled() {
return true;
}
public void setPassword(String password) {
this.password = password;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
編寫登錄認(rèn)證方法JwtUserDetailsServiceImpl.java
該類位于com.viu.technology.service.auth包下(自行建包)
JwtUserDetailsServiceImpl實(shí)現(xiàn)了UserDetailsService接口饰及,SpringSecurity會(huì)去IOC容器中尋找實(shí)現(xiàn)這個(gè)接口的實(shí)現(xiàn)類蔗坯,并將該實(shí)現(xiàn)類作為默認(rèn)的認(rèn)證類。這個(gè)類主要用于獲取用戶身份信息燎含,并不需要我們?nèi)ヅ袛嘤脩裘兔艽a是否匹配宾濒。參照UserDetails實(shí)現(xiàn)的getPassword和getUsername方法。
這里之所要對(duì)username的長(zhǎng)度進(jìn)行判斷是因?yàn)樘闭颍覀兊卿浀臅r(shí)候用的是手機(jī)號(hào)+明文密碼進(jìn)行登錄仓坞,而保存在token里的信息只有id。登錄方法和Token認(rèn)證過濾器都會(huì)調(diào)用loadUserByUsername方法潦匈,所以需要做一個(gè)判斷烘嘱。可能會(huì)有一點(diǎn)疑問尚粘,既然是這樣择卦,為什么不直接用手機(jī)號(hào)做為token的傳遞信息就好了呢,主要還是因?yàn)槲覀兪褂檬謾C(jī)號(hào)查詢的情況比較少郎嫁,而表的主鍵id才是經(jīng)常用的秉继。
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
@Lazy
private UserService userService;
public JwtUserDetailsServiceImpl(){
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = null;
if (username.length() == 32) {
user= userService.getUserAndRoleById(username);
} else if(username.length()==11) {
user= userService.getUserAndRoleByPhone(username);
}
log.info("user:" + user);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
}else{
return user;
}
}
}
編寫賬號(hào)密碼驗(yàn)證失敗處理器EntryPointUnauthorizedHandler.java
位于com.viu.technology.handler包下,自行創(chuàng)建
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
private static Logger log = LoggerFactory.getLogger(EntryPointUnauthorizedHandler.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setStatus(401);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JsonUtil.objectToString(Result.fail(ResultCode.USER_LOGIN_FIAL)));
}
}
編寫賬戶權(quán)限不足處理器RestAccessDeniedHandler.java
位于com.viu.technology.handler包下泽铛,自行創(chuàng)建
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setStatus(403);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JsonUtil.objectToString(Result.fail(ResultCode.USER_PERMISSION_DENIED)));
}
}
編寫Token驗(yàn)證過濾器JwtAuthenticationTokenFilter.java
位于com.viu.technology.filter包下尚辑,自行創(chuàng)建
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
//該字符串作為Authorization請(qǐng)求頭的值的前綴
String tokenHead = "tech-";
if (authHeader != null && authHeader.startsWith(tokenHead)) {
String authToken = authHeader.substring(tokenHead.length());
//從token中獲取userId
String userId = JwtTokenUtil.getUsernameFromToken(authToken);
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//調(diào)用UserDetailsService的認(rèn)證方法(JwtUserDetailsServiceImpl實(shí)現(xiàn)類)
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userId);
//驗(yàn)證token是否正確
if (JwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//將獲取到的用戶身份信息放到SecurityContextHolder中,這個(gè)類是為了在線程中保存當(dāng)前用戶的身份信息
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} else {
log.info("沒有獲取到token");
}
chain.doFilter(request, response);
}
}
配置SpringSecurity
位于com.viu.technology.config.security包下盔腔,自行創(chuàng)建
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟security方法級(jí)別權(quán)限控制注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
@Autowired
private RestAccessDeniedHandler restAccessDeniedHandler;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SimpleUrlAuthenticationSuccessHandler successHandler;
@Autowired
private SimpleUrlAuthenticationFailureHandler failureHandler;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//這里的參數(shù)為不需要認(rèn)證的uri,**代表匹配多級(jí)路徑杠茬,*代表匹配一級(jí)路徑,#代表一個(gè)字符....
.antMatchers(
"/demo/**",
"/user/generate/token"
).permitAll()
//這里表示該路徑需要管理員角色
.antMatchers("/auth/test").hasAnyAuthority("管理員")
.anyRequest().authenticated()
.and()
.headers().cacheControl();
//添加認(rèn)證過濾
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//添加權(quán)限不足及驗(yàn)證失敗處理器
httpSecurity.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);
}
//這個(gè)為SpringSecurity的加密類
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
實(shí)現(xiàn)登錄方法
UserService.java
String login(String phone, String password);
UserServiceImpl.java
這里需要注意一下弛随,UsernamePasswordAuthenticationToken會(huì)自動(dòng)將password進(jìn)行加密之后再比對(duì)瓢喉,而我們之前寫的注冊(cè)用戶方法是以明文方式存入數(shù)據(jù)庫的,并沒有加密舀透,所以我們需要修改一下用戶注冊(cè)方法栓票,然后重新注冊(cè)
public String login(String phone, String password) {
//將用戶名和密碼生成Token
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(phone, password);
//調(diào)用該方法時(shí)SpringSecurity會(huì)去調(diào)用JwtUserDetailsServiceImpl 進(jìn)行驗(yàn)證
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return JwtTokenUtil.generateToken(userDetails);
}
@Autowired
PasswordEncoder passwordEncoder;
public User registerUser(User user) {
//在插入數(shù)據(jù)庫時(shí)將原密碼進(jìn)行加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
User userRes = userDao.insertUser(user);
Role roleRes = roleDao.insertRole(new Role("普通群眾", user.getId()));
List list = new ArrayList();
list.add(roleRes);
if (null != userRes && null != roleRes) {
userRes.setRoles(list);
return user;
}
return null;
}
UserController.java
@PostMapping(value = "/generate/token")
public Result getToken(String phone, String password) throws AuthenticationException {
String token = userService.login(phone, password);
return Result.success(token);
}
測(cè)試獲取token接口
接著我們調(diào)用一下之前寫的注冊(cè)接口,發(fā)現(xiàn)沒發(fā)注冊(cè)愕够,因?yàn)槲覀冊(cè)赟pringSecurity的配置中并沒有開放這個(gè)接口的認(rèn)證走贪,自行添加佛猛。注冊(cè)是不需要用戶身份驗(yàn)證的,否則你讓人家怎么注冊(cè)嘛厉斟。挚躯。。
測(cè)試Token是否能正常使用
UserController.java
@GetMapping("/self/info")
public Result getUserSelfInfo() {
//由于通過驗(yàn)證后我們會(huì)把用戶對(duì)象存到SecurityContextHolder中擦秽,所以這時(shí)候我們能通過下面這句代碼獲取到用戶的身份信息
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return Result.success(user);
}
接下來測(cè)試一下码荔,如果能夠正常獲取就代表成功,記住token前面要加tech-這個(gè)幾個(gè)字符串感挥,看不順眼的話自己去改過濾器
溫馨提醒
你們會(huì)發(fā)現(xiàn)讀出來的數(shù)據(jù)和我稍微有點(diǎn)不一樣對(duì)吧缩搅,哈哈哈哈,肯定啊触幼,你們沒有過濾一下敏感字段(密碼我忘了過濾了0.0)硼瓣,在User類上加入@JSONField(serialize = false)注解即可,SpringBoot會(huì)將持有該注解的字段過濾不進(jìn)行輸出置谦。