通過擴展AccessControlFilter,HashedCredentialsMatcher完成了自定義身份校驗器,訪問控制過濾器等核心技術。
使用了全局業務異常,處理項目中可能出現的異常信息,並使用了枚舉定義輸出信息。
封裝了通用的返回結果集,可在實際開發項目中,直接進行使用。
文章目錄
0、流程圖、思路分析
思路
首先看圖,先理解整合shiro之後如何實現前後端分離的項目
看完圖我們會發現幾個問題
-
爲什麼不使用shiro進行身份認證?
- 因爲這是前後端分離項目,原本的html頁面可能和後臺並不在同一個項目中,使用shiro是訪問不到html頁面的
-
如何儲存用戶認證成功生成的 token ?
- token 可以儲存在session、redis、數據庫中
- 這裏採用 儲存在數據庫中的方式,給數據中添加一個字段 token
- 再設置上 token 的過期時間,添加一個字段 expireDate
-
登錄成功後,再次訪問資源的時候,如何校驗身份?
- 不能使用shiro原本的校驗方式,原本校驗是通過校驗密碼實現的,現在傳遞的驗證方式只有token
- 所以我們需要重寫 密碼校驗器的方法,實現我們自己的驗證方式。
1、代碼實現
1.1、導入依賴
`<dependencies>
<!-- springboot對緩存的支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- ehcache緩存核心 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.4</version>
</dependency>
<!-- shiro對ehcache緩存的支持 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<!-- swagger2 -->
<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>
<!-- druid連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!-- spring對shiro的支持 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
1.2、配置緩存文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" dynamicConfig="false">
<diskStore path="D:\mytemp"/>
<cache name="users"
timeToLiveSeconds="300"
maxEntriesLocalHeap="1000"/>
<defaultCache name="defaultCache"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
maxElementsOnDisk="100000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
1.3、配置application.properties
server.port=8080
spring.application.name=springboot
# mybatis配置
mybatis.type-aliases-package=com.fu.springboot.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
# druid連接池配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql:///demo
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
1.4、編寫swaggerConfig
@SpringBootConfiguration
@EnableSwagger2
public class SwaggerConfig {
private Logger logger = LoggerFactory.getLogger(SwaggerConfig.class);
@Bean
public Docket docket() {
logger.info("Swagger2 ---> docket 執行了...");
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()) // 生成接口文檔的頭部信息
.select() // 表示選擇哪些路徑和API生成文檔,這裏是所有
.apis(RequestHandlerSelectors.basePackage("com.fu.springboot.controller")) // 指定接口所在的包
.paths(PathSelectors.any()) // 表示對所有的API進行監控
.build();
}
/**
* 接口文檔的頭部信息
*/
private ApiInfo apiInfo() {
logger.info("Swagger2 ---> apiInfo 執行了...");
Contact contact = new Contact("孔明", "暫無url", "[email protected]");
return new ApiInfoBuilder()
.title("SpringBoot整合shiro+Swagger2實現前後端分離")
.description("文檔描述")
.contact(contact)
.version("v1.0")
.build();
}
}
1.5、編寫實體類
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 8142836626401616290L;
private Integer id;
private String name;
private String password;
private String token;
private Date expireDate;
}
1.6、編寫返回結果集
public interface ResponseCodeInterface {
/**
* 獲取返回碼
*/
int getCode();
/**
* 獲取返回的消息
*/
String getMsg();
}
public enum BaseResponseCode implements ResponseCodeInterface {
SUCCESS(0, "操作成功"),
SYSTEM_ERROR(500001, "系統錯誤"),
METHOD_INVALIDATE(400001, "數據校驗出錯"),
DATA_ERROR(400002, "傳入數據異常"),
TOKEN_NOT_NULL(401001, "用戶token不存在,請重新登錄"),
TOKEN_ERROR(500002, "用戶身份校驗失敗,請重新登錄");
private int code;
private String msg;
BaseResponseCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
@Override
public int getCode() {
return this.code;
}
@Override
public String getMsg() {
return this.msg;
}
}
@Data
public class DataResult<T> {
/**
* 碼值
*/
private int code = 0;
/**
* 返回的錯誤信息
*/
private String msg = "";
/**
* 返回的數據
*/
private T data;
// 封裝構造器
public DataResult(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public DataResult(int code, T data) {
this.code = code;
this.data = data;
}
public DataResult(int code, String msg) {
this.code = code;
this.msg = msg;
}
public DataResult(){
this.code = BaseResponseCode.SUCCESS.getCode();
this.msg = BaseResponseCode.SUCCESS.getMsg();
this.data = null;
}
public DataResult(T data){
this.code=BaseResponseCode.SUCCESS.getCode();
this.msg=BaseResponseCode.SUCCESS.getMsg();
this.data=data;
}
public DataResult(ResponseCodeInterface responseCodeInterface) {
this.code = responseCodeInterface.getCode();
this.msg = responseCodeInterface.getMsg();
}
public DataResult(ResponseCodeInterface responseCodeInterface, T data) {
this.code = responseCodeInterface.getCode();
this.msg = responseCodeInterface.getMsg();
this.data = data;
}
// 不帶數據的返回值信息
public static <T>DataResult success() {
return new DataResult();
}
// 帶數據的返回值
public static <T>DataResult success(T data) {
return new <T>DataResult(data);
}
// 3個參數的返回值
public static <T>DataResult getResult(int code, String msg, T data) {
return new <T>DataResult(code, msg, data);
}
// 2個參數的返回值(碼值,提示信息)
public static <T>DataResult getResult(int code, String msg) {
return new <T>DataResult(code, msg);
}
// 2個參數的返回值(碼值,用戶信息)
public static <T>DataResult getResult(int code, T data) {
return new <T>DataResult(code, data);
}
/**
* 直接傳遞一個枚舉類型
*/
public static <T>DataResult getResult(BaseResponseCode baseResponseCode) {
return new <T>DataResult(baseResponseCode);
}
public static <T>DataResult getResult(BaseResponseCode baseResponseCode, T data) {
return new <T>DataResult(baseResponseCode, data);
}
}
1.7、編寫全局業務異常
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 3618625760384608631L;
private int messageCode;
private String defaultMessage;
public BusinessException(int messageCode,String defaultMessage){
super(defaultMessage);
this.messageCode=messageCode;
this.defaultMessage=defaultMessage;
}
public String getDefaultMessage() {
return defaultMessage;
}
public int getMessageCode() {
return messageCode;
}
}
1.8、編寫mapper
public interface UserMapper {
/**
* 通過用戶名查詢用戶
*/
User getUserByName(String name);
/**
* 查詢所有的用戶
*/
List<User> getUserAll();
/**
* 更新數據庫用戶的token
*/
void updateUser(User user);
/**
* 查看token是否存在
*/
User getUserByToken(String token);
}
<?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.fu.springboot.mapper.UserMapper">
<!-- 通過用戶名查詢用戶 -->
<select id="getUserByName" resultType="user">
select * from user where name = #{name}
</select>
<!-- 查詢所有的用戶 List<User> getUserAll(); -->
<select id="getUserAll" resultType="user">
select * from user
</select>
<!-- 更新數據庫用戶的token void updateToken(User user); -->
<update id="updateUser" parameterType="user">
update user set token = #{token}, expireDate = #{expireDate} where id = #{id}
</update>
<!-- 查看token是否存在 User getUserByToken(String token); -->
<select id="getUserByToken" resultType="user">
select * from user where token = #{token}
</select>
</mapper>
1.9、編寫service
public interface UserService {
/**
* 根據用戶名獲取用戶信息
*/
User getUserByName(String name);
/**
* 登錄
*/
User login(User user);
/**
* 更新用戶信息
*/
void updateUser(User user);
/**
* 查詢所有的用戶
*/
List<User> getUserList()throws Exception;
/**
* 判定這個token是否存在
*/
boolean tokenExistsOrNot(String token);
}
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User getUserByName(String name) {
return userMapper.getUserByName(name);
}
/**
* 登錄
* 第一步:獲取到前端傳遞過來的用戶名
* 第二步:通過用戶名 獲取用戶對象
* 第三步:校驗
* 第四步:生成token保存到數據庫
* 第五步:將token封裝到返回數據裏面給前端
*/
@Override
public User login(User user) {
String name = user.getName();
User userResult = this.getUserByName(name);
if (null == userResult) { // 說明用戶名不對
throw new BusinessException(400001, "用戶名不正確");
}
if (!userResult.getPassword().equals(user.getPassword())) {
throw new BusinessException(400002, "密碼錯誤");
}
// 生成token,這裏用UUID表示
String token = UUID.randomUUID().toString().substring(0, 30);
Date date = new Date();
userResult.setToken(token);
userResult.setExpireDate(date);
// 更新數據庫的數據
this.updateUser(userResult);
userResult.setPassword("");
return userResult;
}
@Override
public void updateUser(User user) {
userMapper.updateUser(user);
}
@Override
public List<User> getUserList() throws Exception {
return userMapper.getUserAll();
}
@Override
public boolean tokenExistsOrNot(String token) {
try {
User user = userMapper.getUserByToken(token);
if (null != user) {
return true;
}
} catch (Exception e) {
return false;
}
return false;
}
}
1.10、編寫controller
@RestController
@Api(tags = {"用戶接口"})
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("login")
@ApiOperation(value = "用戶登錄的接口")
public DataResult<User> login(@RequestBody User user) {
User user1 = userService.login(user);
DataResult<User> result = null;
try {
result = DataResult.success(user1);
} catch (Exception e) {
if (e instanceof BusinessException) {
BusinessException err = (BusinessException) e;
result = DataResult.getResult(err.getMessageCode(), err.getMessage());
} else {
result = DataResult.getResult(BaseResponseCode.SYSTEM_ERROR.getCode(), BaseResponseCode.SYSTEM_ERROR.getMsg());
}
return result;
}
return result;
}
@GetMapping("list")
@ApiOperation("獲取所有用戶的信息")
@ApiImplicitParam(paramType = "header", name = "token", value = "驗證身份的token", required = true, dataType = "string")
public Object getUserList() {
DataResult<List<User>> result = null;
try {
List<User> list = userService.getUserList();
result = DataResult.success(list);
} catch (Exception e) {
e.printStackTrace();
result = DataResult.getResult(BaseResponseCode.SYSTEM_ERROR.getCode(), BaseResponseCode.SYSTEM_ERROR.getMsg());
}
return result;
}
}
1.11、自定義token
public class CustomToken extends UsernamePasswordToken {
private static final long serialVersionUID = 561721881796304836L;
/**
* 用戶身份唯一的標識
* 這個token是在認證通過之後,用戶訪問其他資源的時候,來進行身份識別的
*/
private String token;
/**
* 只允許在 CustomToken構造的時候給token賦值
*/
public CustomToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
// 在用戶認證通過之後 再訪問這個方法 默認返回的是 Realm校驗的第一個參數
// Realm校驗我們是自己定義的,我們可以自己設置這個方法的返回值
return this.token;
}
}
1.12、編寫UserRealm
public class UserRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(UserRealm.class);
@Override
public String getName() {
return "UserRealm";
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 取出前端傳遞過來的token
CustomToken customToken = (CustomToken) authenticationToken;
String token = (String) customToken.getPrincipal();
// 將前端傳遞過來的token封裝到 SimpleAuthenticationInfo 對象中
SimpleAuthenticationInfo simpleAuthorizationInfo = new SimpleAuthenticationInfo(token, token, getName());
logger.info("UserRealm ---> 認證身份信息執行...");
return simpleAuthorizationInfo;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
logger.info("UserRealm ---> 授權方法執行...");
return simpleAuthorizationInfo;
}
}
1.13、自定義身份校驗器
public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {
@Autowired
private UserService userService;
/**
* @return 返回true代表校驗成功,false代表失敗
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
// 獲取客戶端傳過來的token
CustomToken customToken = (CustomToken) token;
String tokenClient = (String) customToken.getPrincipal();
// 獲取從服務器獲取的token(redis,數據庫 或者 session)
boolean b = false;
try {
b = userService.tokenExistsOrNot(tokenClient);
} catch (Exception e) {
throw new BusinessException(BaseResponseCode.TOKEN_NOT_NULL.getCode(), BaseResponseCode.TOKEN_NOT_NULL.getMsg());
}
// 判斷token是否一致
if (!b) {
throw new BusinessException(BaseResponseCode.TOKEN_ERROR.getCode(), BaseResponseCode.TOKEN_ERROR.getMsg());
}
return true;
}
}
1.14、自定義訪問控制過濾器
public class CustomAccessControlFilter extends AccessControlFilter {
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 校驗身份
try {
// 1. 獲取token
String token = request.getHeader(Constant.REQ_TOKEN);
// 2. 判斷 token 是否爲空
if (StringUtils.isEmpty(token)) { // 用戶的身份是非法的
throw new BusinessException(400004, "用戶請求的token不能爲空");
} else { // 用戶已經登錄,並獲取到了token
// 3. 封裝token
CustomToken customToken = new CustomToken(token);
// 4. 把token交給shiro做認證,判斷身份是否合法
/* 這個方法,用戶第一次訪問請求(即 登錄)的時候,並不會執行
只有在認證成功之後訪問其他資源的時候,纔會執行
作用是:校驗用戶身份,而不是登錄
*/
getSubject(servletRequest, servletResponse).login(customToken);
return true;
}
} catch (BusinessException e) {
// 如果是這個異常:返回JSON告訴客戶端,出現問題了
resultResponse(e.getMessageCode(), e.getDefaultMessage(), servletResponse);
} catch (AuthenticationException e) { // 校驗未通過異常
// e.getCause() :該方法返回的是當前異常的實例
if (e.getCause() instanceof BusinessException) { // 表示返回的是自定義的異常
// 將異常實例進行轉換
BusinessException err = (BusinessException) e.getCause();
resultResponse(err.getMessageCode(), err.getDefaultMessage(), servletResponse);
} else { // 說明是 shiro 拋出的異常
resultResponse(400001, "用戶身份校驗失敗", servletResponse);
}
} catch (AuthorizationException e) { // 授權時出現異常
if (e.getCause() instanceof BusinessException) {
BusinessException err = (BusinessException) e.getCause();
resultResponse(err.getMessageCode(), err.getDefaultMessage(), servletResponse);
} else {
resultResponse(403001, "用戶沒有訪問權限", servletResponse);
}
} catch (Exception e) { // 捕獲未考慮到的異常,比如系統異常
if (e.getCause() instanceof BusinessException) {
BusinessException err = (BusinessException) e.getCause();
resultResponse(err.getMessageCode(), err.getDefaultMessage(), servletResponse);
} else {
resultResponse(500001, "服務器開小差了,系統出錯", servletResponse);
}
}
return false;
}
/**
* 這個方法的主要功能就是告訴客戶端 一些出錯的信息
*/
private void resultResponse(int messageCode, String defaultMessage, ServletResponse servletResponse) {
// 構建返回的數據
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", messageCode);
jsonObject.put("msg", defaultMessage);
// 設置返回的數據類型
/* MediaType.APPLICATION_JSON_UTF8_VALUE ===>>> MediaType.APPLICATION_JSON_VALUE
MediaType.APPLICATION_JSON_UTF8_VALUE 已被標記@Deprecated
自Spring Framework 5.2起不推薦使用,而推薦使用{@link #APPLICATION_JSON_VALUE}
由於主要的瀏覽器(例如Chrome)現在已符合規範並正確解釋了UTF-8特殊字符 不需要{@code charset = UTF-8}參數。
*/
servletResponse.setContentType(MediaType.APPLICATION_JSON.toString());
// 獲取輸出流
try {
ServletOutputStream out = servletResponse.getOutputStream();
// 將數據寫出去
out.write(jsonObject.toJSONString().getBytes());
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.15、編寫shiroConfig
@SpringBootConfiguration
public class ShiroConfig {
private Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
// 自定義密碼認證器
@Bean
public CustomHashedCredentialsMatcher customHashedCredentialsMatcher() {
CustomHashedCredentialsMatcher hashedCredentialsMatcher = new CustomHashedCredentialsMatcher();
logger.info("shiro ---> HashedCredentialsMatcher 執行了...");
return hashedCredentialsMatcher;
}
// 用戶Realm
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(customHashedCredentialsMatcher());
logger.info("shiro ---> UserRealm 執行了...");
return userRealm;
}
// 安全管理器
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(userRealm());
defaultWebSecurityManager.setCacheManager(ehCacheManager());
logger.info("shiro ---> SecurityManager 執行了...");
return defaultWebSecurityManager;
}
// shiro的過濾器
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 配置自定義的校驗身份的過濾器
LinkedHashMap<String, Filter> customAccessControlFilter = new LinkedHashMap<>();
customAccessControlFilter.put("token", new CustomAccessControlFilter());
shiroFilterFactoryBean.setFilters(customAccessControlFilter);
// 配置攔截訪問路徑的過濾器
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("/user/login", "anon");
map.put("/swagger/**","anon");
map.put("/v2/api-docs","anon");
map.put("/swagger-ui.html","anon");
map.put("/swagger-resources/**","anon");
map.put("/webjars/**","anon");
map.put("/favicon.ico","anon");
map.put("/captcha.jpg","anon");
map.put("/csrf","anon");
map.put("/**", "token,authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
logger.info("shiro ---> ShiroFilterFactoryBean 執行了...");
return shiroFilterFactoryBean;
}
// 緩存管理器
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
logger.info("shiro ---> EhCacheManager 執行了...");
return ehCacheManager;
}
/**
* 開啓aop的註解的支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
attributeSourceAdvisor.setSecurityManager(securityManager);
return attributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
}
1.16、編寫AppConfig
@SpringBootApplication
@ComponentScan("com.fu.springboot")
@MapperScan("com.fu.springboot.mapper")
public class AppConfig {
}
1.17、測試
訪問 http://127.0.0.1:8080/swagger-ui.html ,在頁面進行接口測試即可
3、總結
- 首先理解文章開頭的流程圖,理解那幾個問題,然後才能對項目有清晰的思路
- 重點是那兩個過濾器(身份校驗器,訪問流程過濾器),還有返回結果集的封裝,一定要理解清除,那是本項目的核心所在
- 本項目還有很多不足之處,可自行拓展