通过扩展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", "fyf980921@163.com");
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、总结
- 首先理解文章开头的流程图,理解那几个问题,然后才能对项目有清晰的思路
- 重点是那两个过滤器(身份校验器,访问流程过滤器),还有返回结果集的封装,一定要理解清除,那是本项目的核心所在
- 本项目还有很多不足之处,可自行拓展