1. SpringBoot + Mybatis核心框架
2. PageHelper插件 + 通用Mapper插件
3. Shiro + Java-JWT无状态鉴权认证机制
4. Redis(Jedis)缓存框架html
5. PostgreSql前端
彻底使用了 Shiro 的注解配置,保持高度的灵活性。java
放弃 Cookie ,Session ,使用JWT进行鉴权,彻底实现无状态鉴权。mysql
JWT 密钥支持过时时间。web
对跨域提供支持。redis
因为开始是按照mysql方言写的因此建立表时遇到些坑,算法
1.在postgre里user、password是关键字须要加冒号,sql
2.Int自增应该写成serial类型: 数据库
先建立序列,而后设置字段的自增apache
CREATE SEQUENCE users_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
alter table users alter column id set default nextval('users_id_seq');
3.关于外键要直接写在外键后面 role_id int not NULL references role (id)
4.配置文件以下(用的Druid链接池)
5.在作小demo期间还学了Mybatis Generator逆向生成代码:
很好用的偷懒神器,先配置src\main\resources\generator\generatorConfig.xml文件,在项目根目录下(前提是配置了mvn)在IDEA的Maven窗口Plugins中双击执行),可自动生成Model、Mapper、MapperXML。
首先Post用户名与密码到user/login进行登入,若是成功返回一个加密的AccessToken,失败的话直接返回401错误(账号或密码不正确),之后访问都带上这个AccessToken便可,鉴权流程主要是重写了Shiro的入口过滤器JWTFilter(BasicHttpAuthenticationFilter),判断请求
Header里面是否包含Authorization字段,有就进行Shiro的Token登陆认证受权(用户访问每个须要权限的请求必须在Header中添加Authorization字段存放AccessToken),没有就以游客直接访问(有权限管控的话,以游客访问就会被拦截)
主要学习的几个概念:
微服务集群中的每一个服务,对外提供的都使用RESTful风格的接口。而RESTful风格的一个最重要的规范就是:服务的无状态性,即
服务端不保存任何客户端请求者信息
客户端的每次请求必须具有自描述信息,经过这些信息识别客户端身份
客户端请求不依赖服务端的信息,屡次请求不须要必须访问到同一台服务器
服务端的集群和状态对客户端透明
服务端能够任意的迁移和伸缩(能够方便的进行集群化部署)
减少服务端存储压力
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案
客户端接收服务器返回的JWT,将其存储在Cookie中。
此后,客户端将在与服务器交互中都会带JWT。若是将它存储在Cookie中,就能够自动发送,可是不会跨域,所以通常是将它放入HTTP请求的Header Authorization字段中。
Authorization: Bearer
当跨域时,也能够将JWT被放置于POST请求的数据主体中。
JWT头部分是一个描述JWT元数据的JSON对象
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含须要传递的数据。
签名哈希部分是对上面两部分数据签名,经过指定的算法生成哈希,以确保数据不会被篡改。
在网上搜了些关于加密的算法,通常采用MD5+盐的算法,可是当两个用户的明文密码相同时进行加密,会发现数据库中存在相同结构的暗文密码,因此采用AES-128 + Base64是以账号+密码的形式进行加密密码,由于账号具备惟一性,因此也不会出现相同结构的暗文密码这个问题
写了获取当前登陆用户工具类、Json和Object的互相转换的类、jwt工具类
我把工具类粘贴到了博客园http://www.javashuo.com/article/p-znvbvysh-dn.html
AES加密解密工具类、Base64工具是引用博客https://www.jianshu.com/p/f37f8c295057的
关于redis的配置是粘贴http://www.javashuo.com/article/p-zoqgwzgp-db.html的,引用了博主的JedisUtil类
ResponseBean.java
既然想要实现 restful,那咱们要保证每次返回的格式都是相同的,所以创建了一个 ResponseBean 来统一返回的格式。
仿造博客写了一个 CustomUnauthorizedException.java
主要实现了登录、新增用户、经过制定id获取指定用户,其中用到了通用mapper进行查询,刚开始想用前几天看到jpa搜了下看到有相似的通用mapper。
/**
* JWT过滤
* @return
* @author guxiangdong
* @creed: Talk is cheap,show me the code
* @date 2019/9/25 13:59
*/
@RestController
@RequestMapping("/users")
@PropertySource("classpath:config.properties")
public class UserController {
/**
* RefreshToken过时时间
*/
@Value("${refreshTokenExpireTime}")
private String refreshTokenExpireTime;
private final UserUtil userUtil;
private final IUserService userService;
@Autowired
public UserController(UserUtil userUtil, IUserService userService) {
this.userUtil = userUtil;
this.userService = userService;
}
/**
* 获取用户列表
*/
@GetMapping
@RequiresPermissions(logical = Logical.AND, value = {"user:view"})
public ResponseBean user(@Validated BaseDto baseDto) {
if (baseDto.getPage() == null || baseDto.getRows() == null) {
baseDto.setPage(1);
baseDto.setRows(10);
}
PageHelper.startPage(baseDto.getPage(), baseDto.getRows());
List<UsersDto> usersDtos = userService.selectAll();
PageInfo<UsersDto> selectPage = new PageInfo<UsersDto>(usersDtos);
if (usersDtos == null || usersDtos.size() <= 0) {
throw new CustomException("查询失败(Query Failure)");
}
Map<String, Object> result = new HashMap<String, Object>(16);
result.put("count", selectPage.getTotal());
result.put("data", selectPage.getList());
return new ResponseBean(HttpStatus.OK.value(), "查询成功(Query was successful)", result);
}
/**
* 登陆受权
*/
@PostMapping("/login")
public ResponseBean login(@Validated(UserLoginValidGroup.class) @RequestBody UsersDto usersDto, HttpServletResponse httpServletResponse) {
// 查询数据库中的账号信息
UsersDto usersDtoTemp = new UsersDto();
usersDtoTemp.setAccount(usersDto.getAccount());
usersDtoTemp = userService.selectOne(usersDtoTemp);
if (usersDtoTemp == null) {
throw new CustomUnauthorizedException("该账号不存在(The account does not exist.)");
}
// 密码进行AES解密
String key = AesCipherUtil.deCrypto(usersDtoTemp.getPsword());
// 由于密码加密是以账号+密码的形式进行加密的,因此解密后的对比是账号+密码
if (key.equals(usersDto.getAccount() + usersDto.getPsword())) {
// 清除可能存在的Shiro权限信息缓存
if (JedisUtil.exists(Constant.PREFIX_SHIRO_CACHE + usersDto.getAccount())) {
JedisUtil.delKey(Constant.PREFIX_SHIRO_CACHE + usersDto.getAccount());
}
// 设置RefreshToken,时间戳为当前时间戳,直接设置便可(不用先删后设,会覆盖已有的RefreshToken)
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
JedisUtil.setObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN + usersDto.getAccount(), currentTimeMillis, Integer.parseInt(refreshTokenExpireTime));
// // 从Header中Authorization返回AccessToken,时间戳为当前时间戳
String token = JwtUtil.sign(usersDto.getAccount(), currentTimeMillis);
httpServletResponse.setHeader("Authorization", token);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
return new ResponseBean(HttpStatus.OK.value(), "登陆成功(Login Success.)", null);
} else {
throw new CustomUnauthorizedException("账号或密码错误(Account or Password Error.)");
}
}
/**
* 测试登陆
*/
@GetMapping("/article")
public ResponseBean article() {
Subject subject = SecurityUtils.getSubject();
// 登陆了返回true
if (subject.isAuthenticated()) {
return new ResponseBean(HttpStatus.OK.value(), "您已经登陆了(You are already logged in)", null);
} else {
return new ResponseBean(HttpStatus.OK.value(), "你是游客(You are guest)", null);
}
}
/**
* 获取指定用户
*/
@GetMapping("/{id}")
@RequiresPermissions(logical = Logical.AND, value = {"user:view"})
public ResponseBean findById(@PathVariable("id") Integer id) {
UsersDto usersDto = userService.selectByPrimaryKey(id);
if (usersDto == null) {
throw new CustomException("查询失败(Query Failure)");
}
return new ResponseBean(HttpStatus.OK.value(), "查询成功(Query was successful)", usersDto);
}
/**
* 新增用户
*/
@PostMapping("/add")
@RequiresPermissions(logical = Logical.AND, value = {"user:edit"})
public ResponseBean add(@Validated(UserEditValidGroup.class) @RequestBody UsersDto UsersDto ,HttpServletResponse httpServletResponse) {
// 判断当前账号是否存在
UsersDto userDtoTemp = new UsersDto();
userDtoTemp.setAccount(UsersDto.getAccount());
userDtoTemp = userService.selectOne(userDtoTemp);
if (userDtoTemp != null && StringUtil.isNotBlank(userDtoTemp.getPsword())) {
throw new CustomUnauthorizedException("该账号已存在(Account exist.)");
}
UsersDto.setRegTime(new Date());
// 密码以账号+密码的形式进行AES加密
if (UsersDto.getPsword().length() > Constant.PASSWORD_MAX_LEN) {
throw new CustomException("密码最多8位(Psword up to 8 bits.)");
}
String key = AesCipherUtil.enCrypto(UsersDto.getAccount() + UsersDto.getPsword());
UsersDto.setPsword(key);
int count = userService.insert(UsersDto);
if (count <= 0) {
throw new CustomException("新增失败(Insert Failure)");
}
return new ResponseBean(HttpStatus.OK.value(), "新增成功(Insert Success)", UsersDto);
}
}
JWTToken 差很少就是 Shiro 用户名密码的载体。由于先后端分离,服务器无需保存用户状态,因此不须要 RememberMe 这类功能,实现下 AuthenticationToken 接口便可
实现Realm
realm 的用于处理用户是否合法的这一块,须要咱们本身实现。
这里要重写supports方法否则会报错
AuthenticationInfo表明了用户的角色信息集合,AuthorizationInfo表明了角色的权限信息集合,PrincipalCollection是一个身份集合,
全部的请求都会先通过 Filter,因此咱们继承官方的 BasicHttpAuthenticationFilter ,而且重写鉴权的方法代码的执行流程 preHandle(对跨域提供支持) -> isAccessAllowed(登入用户和游客看到的内容是不一样的,若是在这里返回了false,请求会被直接拦截,用户看不到任何东西。因此在这里返回true,Controller中能够经过 subject.isAuthenticated() 来判断用户是否登入若是有些资源只有登入用户才能访问,只须要在方法上面加上 @RequiresAuthentication 注解便可可是这样作有一个缺点,就是不可以对GET,POST等请求进行分别过滤鉴权(由于咱们重写了官方的方法),但实际上对应用影响不大) -> isLoginAttempt (检测Header里面是否包含Authorization字段,有就进行Token登陆认证受权)-> executeLogin (进行登录认证受权)。
@Configuration
public class ShiroConfig {
/**
* 配置使用自定义Realm,关闭Shiro自带的session
* 详情见文档
http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Bean("securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(UsersRealm usersRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 使用自定义Realm
defaultWebSecurityManager.setRealm(usersRealm);
// 关闭Shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
// 设置自定义Cache缓存
defaultWebSecurityManager.setCacheManager(new CustomCacheManager());
return defaultWebSecurityManager;
}
* Shiro自带拦截器配置规则
* rest:好比/admins/user/**=rest[user],根据请求的方法,至关于/admins/user/**=perms[user:method] ,其中method为post,get,delete等
* port:好比/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数
* perms:好比/admins/user/**=perms[user:add:*],perms参数能够写多个,多个时必须加上引号,而且参数之间用逗号分割,好比/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每一个参数都经过才经过,想当于isPermitedAll()方法
* roles:好比/admins/user/**=roles[admin],参数能够写多个,多个时必须加上引号,而且参数之间用逗号分割,当有多个参数时,好比/admins/user/**=roles["admin,guest"],每一个参数经过才算经过,至关于hasAllRoles()方法。//要实现or的效果看http://zgzty.blog.163.com/blog/static/83831226201302983358670/
* anon:好比/admins/**=anon 没有参数,表示能够匿名使用
* authc:好比/admins/user/**=authc表示须要认证才能使用,没有参数
* authcBasic:好比/admins/user/**=authcBasic没有参数表示httpBasic认证
* ssl:好比/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
* user:好比/admins/user/**=user没有参数表示必须存在用户,当登入操做时不作检查
* 详情见文档 http://shiro.apache.org/web.html#urls-
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filterMap = new HashMap<>(16);
filterMap.put("jwt", new JwtFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(16);
filterChainDefinitionMap.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
/**
* 添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引发代理出错的问题,https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
Redis部分尚未太理解,大部分是仿照
https://blog.csdn.net/qq_31897023/article/details/89082541
大概过程
写配置文件config.properties(Redis的配置属性)
JedisConfig.java(JedisPool启动配置Bean,原本是直接将JedisUtil注入为Bean,每次使用直接@Autowired注入使用便可,可是在重写Shiro的CustomCache没法注入JedisUtil,因此就改为静态注入JedisPool链接池,JedisUtil工具类仍是直接调用静态方法,无需@Autowired注入 取自https://blog.csdn.net/W_Z_W_888/article/details/79979103
引用JedisUtil(Jedis工具类)、 StringUtil、 SerializableUtil
重写Shiro的Cache保存读取和Shiro的Cache管理器
重写Shiro的Cache保存读取和Shiro的Cache管理器
CustomCache.java(Cache保存读取)
/**
* 重写Shiro的Cache保存读取
*/
public class CustomCache<K,V> implements Cache<K,V> {
/**
* redis-key-前缀-shiro:cache:
*/
public final static String PREFIX_SHIRO_CACHE = "shiro:cache:";
/**
* 过时时间-5分钟
*/
private static final Integer EXPIRE_TIME = 5 * 60 * 1000;
/**
* 缓存的key名称获取为shiro:cache:account
* @param key
* @return java.lang.String
* @author Wang926454
* @date 2018/9/4 18:33
*/
private String getKey(Object key){
return PREFIX_SHIRO_CACHE + JWTUtil.getUsername(key.toString());
}
/**
* 获取缓存
*/
@Override
public Object get(Object key) throws CacheException {
if(!JedisUtil.exists(this.getKey(key))){
return null;
}
return JedisUtil.getObject(this.getKey(key));
}
/**
* 保存缓存
*/
@Override
public Object put(Object key, Object value) throws CacheException {
// 设置Redis的Shiro缓存
return JedisUtil.setObject(this.getKey(key), value, EXPIRE_TIME);
}
/**
* 移除缓存
*/
@Override
public Object remove(Object key) throws CacheException {
if(!JedisUtil.exists(this.getKey(key))){
return null;
}
JedisUtil.delKey(this.getKey(key));
return null;
}
/**
* 清空全部缓存
*/
@Override
public void clear() throws CacheException {
JedisUtil.getJedis().flushDB();
}
/**
* 缓存的个数
*/
@Override
public int size() {
Long size = JedisUtil.getJedis().dbSize();
return size.intValue();
}
/**
* 获取全部的key
*/
@Override
public Set keys() {
Set<byte[]> keys = JedisUtil.getJedis().keys(new String("*").getBytes());
Set<Object> set = new HashSet<Object>();
for (byte[] bs : keys) {
set.add(SerializableUtil.unserializable(bs));
}
return set;
}
/**
* 获取全部的value
*/
@Override
public Collection values() {
Set keys = this.keys();
List<Object> values = new ArrayList<Object>();
for (Object key : keys) {
values.add(JedisUtil.getObject(this.getKey(key)));
}
return values;
}
}
CustomCacheManager.java(缓存(Cache)管理器)
/**
* 重写Shiro缓存管理器
*/
public class CustomCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return new CustomCache<K,V>();
}
}
最后在Shiro的配置Bean里设置咱们重写的缓存(Cache)管理器
登陆认证经过后返回AccessToken信息(在AccessToken中保存当前的时间戳和账号),同时在Redis中设置一条以账号为Key,Value为当前时间戳(登陆时间)的RefreshToken,如今认证时必须AccessToken没失效以及Redis存在所对应的RefreshToken,且RefreshToken时间戳和AccessToken信息中时间戳一致才算认证经过,这样能够作到JWT的可控性,若是从新登陆获取了新的AccessToken,旧的AccessToken就认证不了,由于Redis中所存放的的RefreshToken时间戳信息只会和最新的AccessToken信息中携带的时间戳一致,这样每一个用户就只能使用最新的AccessToken认证,Redis的RefreshToken也能够用来判断用户是否在线,若是删除Redis的某个RefreshToken,那这个RefreshToken所对应的AccessToken以后也没法经过认证。
自己AccessToken的过时时间为5分钟(配置文件可配置),RefreshToken过时时间为30分钟(配置文件可配置),当登陆后时间过了5分钟以后,当前AccessToken便会过时失效,再次带上AccessToken访问JWT会抛出TokenExpiredException异常说明Token过时,开始判断是否要进行AccessToken刷新,首先Redis查询RefreshToken是否存在,以及时间戳和过时AccessToken所携带的时间戳是否一致,若是存在且一致就进行AccessToken刷新,过时时间为5分钟(配置文件可配置),时间戳为当前最新时间戳,同时也设置RefreshToken中的时间戳为当前最新时间戳,刷新过时时间从新为30分钟过时(配置文件可配置),最终将刷新的AccessToken存放在Response的Header中的Authorization字段返
回(前端进行获取替换,下次用新的AccessToken进行访问)
先设置Content-Type为application/json
而后填写请求参数账号密码信息,进行请求访问,请求访问成功
点击查看Header信息的Authorization属性便是Token字段
访问须要权限的请求将Token字段放在Header信息的Authorization属性访问便可
新增用户也须要权限的请求将Token字段放在Header信息的Authorization属性访问便可
最后,目前本身最大问题就是能够理解代码,可是一旦本身动手作的时候就犯难,过于依赖网上搬砖,业余须要多加联系。
[顾祥东1]配置Redis