自上一篇文章的基礎上,Spring Boot 鑒權之—— JWT 鑒權我做了一波springboot2.0.4+mybatis 的整合。
參考文章: Spring Boot+Spring Security+JWT 實現 RESTful Api 權限控制
源碼地址:
碼雲:https://gitee.com/region/spring-security-oauth-example/tree/master/spring-security-jwt
springboot2.0.4+mybatis pom.xml:
這里由於springboot2.0.4沒有默認的passwordencoder,也就是說我們登錄不能明文登錄,所以為了方便期間,我直接使用了數據庫。
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!-- Spring-Mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <!-- MySQL 連接驅動依賴 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
config配置修改:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import com.jwt.server.filter.JwtAuthenticationFilter; import com.jwt.server.filter.JwtLoginFilter; import com.jwt.server.provider.CustomAuthenticationProvider; /** * 通過SpringSecurity的配置,將JWTLoginFilter,JWTAuthenticationFilter組合在一起 * * @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) 在springboot1.5.8的時候該注解是可以用的 * 具體看源碼 * @author zyl * */ @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Qualifier("userDetailServiceImpl") @Autowired private UserDetailsService userDetailsService; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity http) throws Exception { // 自定義 默認 http.cors().and().csrf().disable().authorizeRequests().antMatchers("/users/signup").permitAll().anyRequest() .authenticated().and().addFilter(new JwtLoginFilter(authenticationManager()))// 默認登錄過濾器 .addFilter(new JwtAuthenticationFilter(authenticationManager()));// 自定義過濾器 } // 該方法是登錄的時候會進入 @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { // auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()); // 使用自定義身份驗證組件 手動注入加密類 auth.authenticationProvider(new CustomAuthenticationProvider(userDetailsService, bCryptPasswordEncoder)); } }
自定義身份驗證組件
package com.jwt.server.provider; import java.util.ArrayList; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** * 自定義身份認證驗證組件 * @author zyl * */ public class CustomAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; private BCryptPasswordEncoder bCryptPasswordEncoder; public CustomAuthenticationProvider(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder){ this.userDetailsService = userDetailsService; this.bCryptPasswordEncoder = bCryptPasswordEncoder; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 獲取認證的用戶名 & 密碼 String name = authentication.getName(); String password = authentication.getCredentials().toString(); // 認證邏輯 UserDetails userDetails = userDetailsService.loadUserByUsername(name); if (null != userDetails) { if (bCryptPasswordEncoder.matches(password, userDetails.getPassword())) { // 這里設置權限和角色 ArrayList<GrantedAuthority> authorities = new ArrayList<>(); authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN")); authorities.add( new GrantedAuthorityImpl("ROLE_API")); authorities.add( new GrantedAuthorityImpl("AUTH_WRITE")); // 生成令牌 這里令牌里面存入了:name,password,authorities, 當然你也可以放其他內容 Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities); return auth; } else { throw new BadCredentialsException("密碼錯誤"); } } else { throw new UsernameNotFoundException("用戶不存在"); } } /** * 是否可以提供輸入類型的認證服務 * @param authentication * @return */ @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
權限類型,負責存儲權限和角色
package com.jwt.server.provider; import org.springframework.security.core.GrantedAuthority; /** * 權限類型,負責存儲權限和角色 * * @author zyl */ public class GrantedAuthorityImpl implements GrantedAuthority { /** * */ private static final long serialVersionUID = 1L; private String authority; public GrantedAuthorityImpl(String authority) { this.authority = authority; } public void setAuthority(String authority) { this.authority = authority; } @Override public String getAuthority() { return this.authority; } }
定義數據庫service、dao文件
package com.jwt.server.service; import com.jwt.server.domain.UserInfo; /** * 用戶service * @author zyl * */ public interface UserService { /** * 根據用戶名查詢用戶是否存在 * @param username * @return */ public UserInfo findByUsername(String username); /** * 添加用戶 * @param user * @return */ public UserInfo save(UserInfo user); }
package com.jwt.server.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.jwt.server.domain.UserInfo; import com.jwt.server.mapper.UserMapper; import com.jwt.server.service.UserService; @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper usermapper; @Override public UserInfo findByUsername(String username) { return usermapper.findByUsername(username); } @Override public UserInfo save(UserInfo user) { return usermapper.save(user); } }
修改之前定義的UserDetailServiceImpl文件為:
package com.jwt.server.service.impl; import static java.util.Collections.emptyList; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import com.jwt.server.domain.UserInfo; import com.jwt.server.service.UserService; /** * * @author zyl * */ @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired protected UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserInfo user = userService.findByUsername(username); if (user == null) { throw new UsernameNotFoundException(username); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), emptyList()); } }
增加IdGenerator id生成類
package com.jwt.server.util; import java.net.InetAddress; import java.net.UnknownHostException; import org.apache.commons.lang3.time.DateFormatUtils; import lombok.extern.slf4j.Slf4j; /** * 與snowflake算法區別,返回字符串id,占用更多字節,但直觀從id中看出生成時間 * */ @Slf4j public enum IdGenerator { /** * 每個要生成的序號類型對應一個序號 */ USER_TRANSID("1"); private long workerId; //用ip地址最后幾個字節標示 private long datacenterId = 0L; //可配置在properties中,啟動時加載,此處默認先寫成0 private long sequence = 0L; private final long twepoch = 1516175710371L; private final long workerIdBits = 1L; private final long datacenterIdBits = 2L; private final long sequenceBits = 3L; private final long workerIdShift = sequenceBits; private final long datacenterIdShift = sequenceBits + workerIdBits; private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095 private long lastTimestamp = -1L; private String index; IdGenerator(String ind) { this.index = ind; workerId = 0x000000FF & getLastIP(); } public synchronized String nextId() { long timestamp = timeGen(); //獲取當前毫秒數 //如果服務器時間有問題(時鍾后退) 報錯。 if (timestamp < lastTimestamp) { throw new RuntimeException(String.format( "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } //如果上次生成時間和當前時間相同,在同一毫秒內 if (lastTimestamp == timestamp) { //sequence自增,因為sequence只有12bit,所以和sequenceMask相與一下,去掉高位 sequence = (sequence + 1) & sequenceMask; //判斷是否溢出,也就是每毫秒內超過4095,當為4096時,與sequenceMask相與,sequence就等於0 if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); //自旋等待到下一毫秒 } } else { sequence = 0L; //如果和上次生成時間不同,重置sequence,就是下一毫秒開始,sequence計數重新從0開始累加 } lastTimestamp = timestamp; long suffix = ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; String datePrefix = DateFormatUtils.format(timeGen(), "yyyyMMddHHmmss"); return datePrefix +index + suffix; } private long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } private long timeGen() { return System.currentTimeMillis(); } private byte getLastIP(){ byte lastip = 0; try{ InetAddress ip = InetAddress.getLocalHost(); byte[] ipByte = ip.getAddress(); lastip = ipByte[ipByte.length - 1]; } catch (UnknownHostException e) { log.error("UnknownHostException error:{}", e.getMessage()); } return lastip; } public static void main(String[] args) { IdGenerator id = IdGenerator.USER_TRANSID; for (int i = 0; i < 1000; i++) { String serialNo = id.nextId(); System.out.println(serialNo + "===" + serialNo.length()); } } }
mapper
package com.jwt.server.mapper; import com.jwt.server.domain.UserInfo; public interface UserMapper { /** * 根據用戶名查詢用戶是否存在 * * @param username * @return */ public UserInfo findByUsername(String username); /** * 添加用戶 * * @param user * @return */ public UserInfo save(UserInfo user); }
mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jwt.server.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.jwt.server.domain.UserInfo">
<id column="id" jdbcType="VARCHAR" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
</resultMap>
<!--用戶登錄查詢 -->
<select id="findByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
select id,username,password from tb_user where username=#{username,jdbcType=VARCHAR}
</select>
<insert id="save" parameterType="com.jwt.server.domain.UserInfo">
INSERT INTO tb_user
(id,username,password) VALUES
(#{id,jdbcType=VARCHAR},#{username,jdbcType=VARCHAR},#{password,jdbcType=VARCHAR})
</insert>
</mapper>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias alias="Integer" type="java.lang.Integer" />
<typeAlias alias="Long" type="java.lang.Long" />
<typeAlias alias="HashMap" type="java.util.HashMap" />
<typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap" />
<typeAlias alias="ArrayList" type="java.util.ArrayList" />
<typeAlias alias="LinkedList" type="java.util.LinkedList" />
</typeAliases>
</configuration>
application.yml配置
#公共配置與profiles選擇無關 mapperLocations指的路徑是src/main/resources mybatis: typeAliasesPackage: com.jwt.server.domain mapperLocations: classpath:mapper/*.xml --- #開發配置 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/test?prepStmtCacheSize=517&cachePrepStmts=true&autoReconnect=true&characterEncoding=utf-8&allowMultiQueries=true username: root password: tiger
修改啟動類掃描包
package com.jwt.server; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @SpringBootApplication @MapperScan("com.jwt.server.mapper")// public class SpringJwtApplication { public static void main(String[] args) { SpringApplication.run(SpringJwtApplication.class, args); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } }
測試:

自定義登錄測試:

好了ok啦。
需要注意的是:

在springboot2.0.4版本的時候由於沒有默認的passwordencoder,因此需要手動注入。如果不注入會在鑒權的時候報如下錯誤

如果測試會會有如下情況,說明你注入后未給密碼加密

並且這里如果沒有存儲我們登錄的信息時,可能也會有個坑,就是密碼加密后與原密碼做對比會報如下錯誤

一般情況下我們用加密后,在授權的時候回去對比密碼

這個錯誤就是會在這個地方產生的。解決辦法
自定義身份驗證類

自行調用,確保密碼一致就ok。具體請看源碼分析。
