gitee源码地址:https://gitee.com/bai-yaofeng/springboot002
service模块和common模块主要都是存放公共信息的,service存放的是pojo,这里就不展开了。
而sso模块则是定义登录规则的,主要存放shiro认证和权限校验,manager模块是管理模块的
这里把全部粘下去了,自己看对应版本就行
<!--定义版本号-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<mybatis-plus.version>3.3.2</mybatis-plus.version>
<velocity.version>2.0</velocity.version>
<lombok.version>1.16.20</lombok.version>
<swagger.version>2.7.0</swagger.version>
<jodatime.version>2.10.1</jodatime.version>
<commons-fileupload.version>1.3.1</commons-fileupload.version>
<commons-beansutils.version>1.9.3</commons-beansutils.version>
<commons-io.version>2.6</commons-io.version>
<httpclient.version>4.5.2</httpclient.version>
<fastjson.version>1.2.28</fastjson.version>
<poi.version>3.17</poi.version>
<mysql.version>8.0.11</mysql.version>
<druid.version>1.1.10</druid.version>
<redis.version>2.9.2</redis.version>
<shiro-spring.version>1.4.0</shiro-spring.version>
<shiro-redis.version>2.4.2.1-RELEASE</shiro-redis.version>
<shiro-spring-boot-web-starter.version>1.6.0</shiro-spring-boot-web-starter.version>
<jwt.version>3.2.0</jwt.version>
<pinyin.version>2.5.0</pinyin.version>
</properties>
<dependencies>
<!--mybatis-plus 持久层-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<!-- druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- Mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Lombok 插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${redis.version}</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>${commons-beansutils.version}</version>
</dependency>
<!--springboot整合shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.1</version>
</dependency>
整合登录模块的主要流程是:
自定义shiro配置类:
1.配置shiro,配置其内置过滤,进行自定义的JWT做拦截器进行拦截校验
2.配置SecurityManager,并且将安全管理器作为参数交给shiroFilter管理
3.配置自定义的realm(用来定义验证和权限的),作为参数交给SecurityManager管理,实现redis缓存处理
/**
* shiro配置类
*/
@Configuration
public class ShiroConfig {
/**
* 步骤1:配置shiro,将所有请求提交给shiro去管理
* 添加shiro自带的内置过滤器
* 常用过滤器:
* anon:无需认证(登录)即可访问
* authc:必须认证才可以访问
* user:如果使用rememberMe的功能可以直接访问
* perms:该资源必须获得资源权限才能使用
* role:该资源必须得到用户角色权限才能访问
* @param defaultWebSecurityManager 安全管理器
* @return
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//这里不使用shiro自带的拦截器,使用自定义的jwt来进行拦截
HashMap<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//设置公共资源进行放行,不需要验证
HashMap<String, String> map = new HashMap<>();
map.put("/user/login", "anon"); //anno设置为公共资源,不需要认证,注意不受限的资源要放在上边
map.put("/user/registry", "anon"); //用户通过注册页面,注册信息跳转到注册接口,需要放行
map.put("/user/getAuthorizationInfo","anon"); //根据token校验信息
//拦截请求使用我们自己定义的拦截器
map.put("/**", "jwt");
//将规则交给shiro
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 步骤2.配置SecurityManager,并且将安全管理器作为参数交给shiroFilter管理 web环境下 创建DefaultWebSecurityManager
* @param realm 自定义realm
* @return
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("realm") Realm realm){
//给安全管理器设置自定义的realm,从bean中获取到的传递过来的
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
/**
* 3.配置自定义的realm(用来定义验证和权限的),作为参数交给SecurityManager管理,实现redis缓存处理
* @return
*/
@Bean("realm")
public Realm getRealm(){
//创建自定的realm
MyRealm myRealm=new MyRealm();
//开启缓存管理避免了多次反复查询数据库
myRealm.setCacheManager(new RedisCacheManager()); //自定义的redis缓存也可以通过实现CacheManager做shiro缓存
myRealm.setCachingEnabled(true); //开启缓存
myRealm.setAuthenticationCachingEnabled(true); //开启认证缓存
myRealm.setAuthenticationCacheName("authenticationCache"); //设置认证缓存的名称,方便查找 (默认名称:authentication + 自定义realm名称)
myRealm.setAuthorizationCachingEnabled(true); //开启授权缓存
myRealm.setAuthorizationCacheName("authorizationCache"); //设置授权缓存名称*/
return myRealm;
}
/**
* 自定义realm,授权的认证
*/
public class MyRealm extends AuthorizingRealm {
public String JWT_TOKEN = "token_jwt_";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UserService userService;
@Autowired
private UserRoleService userRoleService; //用户-角色表
@Autowired
private RoleService roleService; //角色表
@Autowired
private PermissionService permissionService; //权限表
@Autowired
private RolePermissionService rolePermissionService; //角色-用户表
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//TODO 授权先不做,后续补充
System.out.println("-------执行权限逻辑------");
//TODO 获取当前登录用户的账号名,后续可修改成mybatis中sql连接的方式
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
//查询当前的用户角色
User username = userService.getOne(new QueryWrapper<User>().eq("username", primaryPrincipal));
List<UserRole> userRoles = userRoleService.list(new QueryWrapper<UserRole>().eq("user_id", username.getUId()));
if (userRoles.size()!=0){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
userRoles.forEach(userRole->{
Role role = roleService.getOne(new QueryWrapper<Role>().eq("r_id", userRole.getRoleId()));
simpleAuthorizationInfo.addRole(role.getRoleName()); //添加角色给shiro
//根据角色id,查询多个权限信息添加到shiro中去
List<RolePermission> rolePermissionList = rolePermissionService.list(new QueryWrapper<RolePermission>().eq("role_id", role.getRId()));
rolePermissionList.forEach(rolePermission -> {
Permission permission = permissionService.getOne(new QueryWrapper<Permission>().eq("pId", rolePermission.getPermissionId()));
simpleAuthorizationInfo.addStringPermission(permission.getPermissionName()); //添加权限代码给shiro
});
});
return simpleAuthorizationInfo;
}
return null;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@SneakyThrows
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
System.out.println("-------执行认证逻辑------");
String token = (String) authenticationToken.getPrincipal();
//获取到的token是jwt加密后的token.这里需要去解析判断是否符合或者为空,符合规则则继续进行流程
DecodedJWT decodedJWT = (DecodedJWT) jwtUtil.verifyToken(token).getData().get("jwt");
if (decodedJWT == null){
throw new MyException(ExceptionEnums.INVALID_Token);
}
String userName = String.valueOf(decodedJWT.getClaim("userName").asString());
//根据用户名查询数据库信息,不想进行数据库操作的话可直接用jwt存储的信息的来判断
User user = userService.getOne(new QueryWrapper<User>().eq("username", userName));
if (user == null) {
throw new UnknownAccountException("账户不存在");
}
//获取redis中对应用户的token信息,用户登录的时候会在redis中存储一个后缀名为username的jwt
String redisUserToken = (String) redisTemplate.opsForValue().get(JWT_TOKEN+userName);
try{
jwtUtil.verifyToken(token);
}catch (Exception e){
e.printStackTrace();
}
if (redisUserToken == null) {
throw new MyException(ExceptionEnums.INVALID_Token);
}
return new SimpleAuthenticationInfo(user.getUsername(), authenticationToken.getCredentials().toString(), this.getName());
}
/**
* 建议重写此方法,提供唯一的缓存Key
*/
@Override
protected Object getAuthorizationCacheKey(PrincipalCollection principals) {
String userName = (String) principals.getPrimaryPrincipal();
return userName;
}
/**
* 建议重写此方法,提供唯一的缓存Key
*/
@SuppressWarnings("unchecked")
@Override
protected Object getAuthenticationCacheKey(PrincipalCollection principals) {
String userName = (String) principals.getPrimaryPrincipal();
return userName;
}
}
具体想知道jwt的话可上网了解相关知识,简单来讲就是一个可以包含对象信息的加密串,由三部分组合而成,减少数据库校验的次数
/**
* jwt自定义拦截器
*/
@Slf4j
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter {
@SneakyThrows
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断请求的请求头是否带上 "Token"
String jwtToken = ((HttpServletRequest) request).getHeader("token");
if (jwtToken != null) {
//有token,从redis中获取,看是否为已退出用户的token
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
if (redisTemplate.opsForValue().get("logout_" + jwtToken) != null) {
System.out.println(redisTemplate.opsForValue().get("logout_" + jwtToken));
System.out.println("退出用户的token在redis中存在");
String result = ResultException.getResult("400", "token无效");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(result);
}
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
log.info("token存在");
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
/*//统一定义成无效token
String result = ResultException.getResult("400", "无效token");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(result);*/
return false;
}
}
//测试,如果不存在token的话,说明当前用户并没有进行登录,则返回错误信息,提示必须先登录再进行访问
String result = ResultException.getResult("401", "当前并未登录,无法访问");
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write(result);
return false;
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
//return true;
}
/**
* 将非法请求跳转到 /filterError/**中
*/
private void responseError(ServletResponse response, int code, String message) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//设置编码,否则中文字符在重定向时会变为空字符串
message = URLEncoder.encode(message, "UTF-8");
//如果有项目名称路径记得加上
httpServletResponse.sendRedirect("/filterError/" + code + "/" + message);
} catch (IOException e1) {
log.error(e1.getMessage());
}
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("token");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
//跨域支持
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
实现shiro+jwt+redis最主要的就是这三个类,其中还有一些异常处理类和工具类,需要的话可以去源码中查看
主要实现流程就是:用户注册的时候生成一个加密的jwt,里面包含用户的username,以及用在redis中存储一个以用户名结尾的key值,如(token_jwt_xiaobai),然后每次访问其他接口的时候加上一个token,这个token就是redis里面存储的那个jwt串。接口会给shiro拦截,交给自定义的realm去做认证校验,根据对比传入的token和redis存储的token,来判断是否一致,以及token的合法性,是否过期等情况,进行校验
管理服务的话主要是编写自己主要的业务逻辑,每个接口都要由sso模块进行认证和权限的校验。
由于我这里用的不是分布式的,所以调用拦截的时候不能利用网关来实现,只能自己在定义过一个拦截器,把拦截请求下来交给sso去做校验
/**
* 拦截器,拦截请求,将请求自带的jwtToken拿去sso那里去做校验
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginStub loginStub;
/**
* controller执行前所进行的操作
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setContentType("application/json; charset=UTF-8");
//判断请求头的token是否存在
String token = request.getHeader("token");
if (token != null){
//判断当前token是否符合redis和jwt的验证
Result result = loginStub.getUserByToken(token);
if (result.getCode() == 0){
//认证完之后进行授权认证,空的时候放行和授权成功的话则放行,有权限校验而且权限不符合的时候进行拦截
AuthCheck authCheck = ((HandlerMethod) handler).getMethod().getAnnotation(AuthCheck.class);
if (authCheck == null) {
return true;
}
String admin = authCheck.user();
List<String> roleList = (List<String>) result.getData().get("roleList");
if (roleList != null){
if (roleList.contains(admin)){
return true;
}else {
response.getWriter().write("并无当前权限,访问已被拦截");
return false;
}
}
}else {
String errorMessage = (String) result.getData().get("errorMessage");
response.getWriter().write(errorMessage);
return false;
}
}
response.getWriter().write("token无效,请重新登录");
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
/**
* mvc拦截器
*/
@Configuration
public class LoginConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册LoginInterceptor拦截器
InterceptorRegistration registration = registry.addInterceptor(loginInterceptor);
registration.addPathPatterns("/**"); //所有路径都被拦截
registration.excludePathPatterns( //添加不拦截路径
"/druid/*" //数据库资源
);
}
}
调用sso的校验信息接口,获得返回值做二次拦截校验
/**
* 处理token登录
*/
@Service
public class LoginStub {
@Autowired
private RestTemplate restTemplate;
/**
* 根据token查询当前用户是否真实
* @param token
* @return
*/
public Result getUserByToken(String token){
//调用sso中根据token校验用户的接口,后续可做修改
ResponseEntity<Result> entity = restTemplate.postForEntity("http://localhost:8082/user/getAuthorizationInfo", token, Result.class);
//获取接口返回的信息
Result rs=new Result();
rs.setData(entity.getBody().getData());
rs.setCode(entity.getBody().getCode());
return rs;
}
}
管理端的权限拦截我是用注解的方式来校验的,所以创了个自定义注解来获取所需的权限,再跟用户认证完成后得到的权限做比较。