1 Shiro模块说明:
1.1 Shiro的概述:
(1) Shiro和SpringSecurity是权限管理的两个主流框架。
(2) 权限管理包括 1) 用户身份认证;2) 用户授权 两部分。对需要进行访问控制的用户首先需要经过身份验证,认证通过后的用户具有指定资源的访问权限才可以访问。
1.2 本次实现中Shiro的作用:
(1) 在用户未登录之前只能访问登录注册页面,登录之后可以访问商品页面。
1.3 Shiro框架的整合:
本次实现了项目与Shiro的用户身份认证功能的整合–shoppingProject01_pub : version5.0.1,具体过程如下:
Step1:引依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-starterartifactId>
<version>1.5.3version>
dependency>
Step2:整合Shiro框架
com.salieri.config.ShiroConfig:
// 用来整合shiro框架相关的配置类
@Configuration
public class ShiroConfig {
// 配置ShiroDialect:用于thymeleaf和shiro标签配合使用
// 要加这个bean,shiro标签才能在thymeleaf中起效果
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
// 1. 创建shiroFilter,负责拦截所有请求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 配置系统受限资源
// 配置系统公共资源
Map<String, String> map = new HashMap<>();
map.put("/user/*", "anon"); // anon 设置为公共资源,负责放行资源
map.put("/css/*", "anon");
map.put("/img/*", "anon");
map.put("/js/*", "anon");
map.put("/login.html", "anon");
map.put("/registEmail.html", "anon");
map.put("/registPhone.html", "anon");
map.put("/registNickName.html", "anon");
map.put("/test02.html", "anon");
map.put("/item/alipay/notifyUrl","anon");
map.put("/**", "authc"); // authc 请求这个资源需要认证和授权
// 默认认证界面路径
shiroFilterFactoryBean.setLoginUrl("/login.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
// 2. 创建安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
// 3. 创建自定义realm
@Bean
public Realm getRealm() {
UserRealm userRealm = new UserRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为md5
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置散列次数
credentialsMatcher.setHashIterations(1024);
userRealm.setCredentialsMatcher(credentialsMatcher);
return userRealm;
}
}
Step3: 创建自定义Realm
con.salieri.shiro.realms.UserRealm
// 自定义realm
public class UserRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
// 身份验证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("=========");
// 获取身份信息(由UserServiceImpl,loginAccount传入)
String principal = (String) token.getPrincipal();
// 在工厂中获取service对象
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
List<User> userList = userService.selectUserByName(principal);
User user = userList.get(0);
if ( !ObjectUtils.isEmpty(user) ) {
return new SimpleAuthenticationInfo(user.getName(), user.getPassword(), new MyByteSource(user.getSalt()), this.getName());
}
return null;
}
}
Step4: 创建工厂类对象
com.salieri.utils.ApplicationContextUtils
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override // springboot启动时会将工厂以参数形式回传给该方法
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
// 由此可根据bean名字获取工厂中指定的bean对象
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
1.4 基于Shiro的用户认证过程的实现:
1 ) 用户注册账号时,对明文密码进行md5+salt+hash散列
// 注册账号
@Override
public int createAccount(User user,String confirmCode) {
// 判断激活的key
this.activeKey = "ActiveCode" + user.getName() + ":isAlive";
// // 过期时间为5分钟
// redisTemplate.opsForValue().set(activeKey,"alive",5, TimeUnit.MINUTES);
// 生成随机盐(用于加密)
String salt = SaltUtils.getSalt(8);
// 明文密码进行md5+salt+hash散列
Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
// 初始化账号信息
user.setSalt(salt);
user.setPassword(md5Hash.toHex());
user.setConfirmCode(confirmCode);
user.setIsValid((byte) 0);
return userDAO.insertUser(user);
}
2 )用户登录账号时,利用Shiro方法做如下判断:
// 情况3:查询到一个用户时
// 获取主体对象
try {
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(user.getName(), user.getPassword()));
map.put("state",true);
map.put("msg","登录成功");
return map;
} catch ( UnknownAccountException e ) {
e.printStackTrace();
map.put("state",false);
map.put("msg","用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
map.put("state",false);
map.put("msg","密码错误");
} catch (Exception e) {
e.printStackTrace();
map.put("state",false);
map.put("msg","登录失败");
}
return map;
}
此时 UsernamePasswordToken 会调用 com.salieri.config.ShiroConfig ,实现解密验证。
3 )用户退出账号时,利用如下代码段实现认证权限退出:
public Map<String,Object> logout() {
Map<String,Object> map = new HashMap<>();
try {
Subject subject = SecurityUtils.getSubject();
subject.logout(); // 退出用户
map.put("state",true);
map.put("msg","提示:退出账户成功");
return map;
} catch (Exception e) {
e.printStackTrace();
map.put("state",false);
map.put("msg","提示:退出账户失败");
}
return map;
}
2 JWT模块说明:
2.1 JWT的概述:
2.1.1 服务器端利用session&cookie存储用户信息存在问题如下:
2.1.2 JWT认证流程:
Step1: 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
Step2:后端核对用户名和密码成功后,将用户的id等其他信息作为JTW Payload(负载),将其余头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。 token格式:head.payload.singurater
Step3:后端将JWT字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
Step4:后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确:检查Token是否过期;检查Token的接收方是否是自己(可选)。
Step5:验证通过后,后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
2.1.3 JWT优势:
1)) 简洁(Compact):可以通过URL,POST参数或者在HTPP header发送,因为数据量小,传输速度也很快。
2)) 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。
3)) 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
4)) 不需要在服务端保存会话信息,特别适用于分布式微服务。
4 ) JWT结构:
2.1.4 加拦截器:
给接口加token验证确实能对接口起到一定的保护,但每个接口都做JWT,代码冗余度太高,故在单体应用下,可用拦截器去做。在分布式系统中,可用网关去解决。
2.2 本次实现中JWT的作用:
(1) 在用户未登录之前只能访问登录注册页面,登录之后可以访问商品页面。
2.3 JWT的整合过程:
本次实现了项目与JWT的用户身份认证功能的整合–shoppingProject01_pub : version5.0.2,具体过程如下:
Step1: 创建JWTUtils工具类
public class JWTUtils {
private static final String SIGN = SaltUtils.getSalt(6);
// 生成token ----> header.payload.sign
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7); // 默认7天过期
// 创建jwt builder
JWTCreator.Builder builder = JWT.create();
// payload
map.forEach((k,v)->{
builder.withClaim(k,v);
});
String token = builder.withExpiresAt(instance.getTime()) // 指定过期时间
.sign(Algorithm.HMAC256(SIGN)); // 签名
return token;
}
// 验证token合法性,获取token信息
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
}
Step2: 创建JWT拦截器
com.salieri.interceptors.JWTInterceptor
// 拦截器,HandlerInterceptor由springmvc提供
public class JWTInterceptor implements HandlerInterceptor {
// 预处理拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String,Object> map = new HashMap<>();
// 获取请求头中的令牌
String token = request.getHeader("token");
try {
DecodedJWT verify = JWTUtils.verify(token); // 验证令牌
return true; // 放行请求
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg","无效签名");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg","token过期");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg","token算法不一致");
} catch (Exception e) {
e.printStackTrace();
map.put("msg","token无效");
}
map.put("state",false); // 设置状态
// 将map转为json
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
Step3: 配置授权页面
com.salieri.config.InterceptorConfig
@Configuration // 为了被springboot扫描到
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/**") // 其他接口token验证保护
.excludePathPatterns("/user/**") // 应该所有用户都放行
.excludePathPatterns("/login.html")
.excludePathPatterns("/registEmail.html")
.excludePathPatterns("/registNickName.html")
.excludePathPatterns("/registPhone.html")
.excludePathPatterns("/itemlist.html")
.excludePathPatterns("/item.html")
.excludePathPatterns("/activation-account.html")
.excludePathPatterns("/successpay.html")
.excludePathPatterns("/swagger-ui.html")
.excludePathPatterns("/item/payForItem")
.excludePathPatterns("/css/*")
.excludePathPatterns("/img/*")
.excludePathPatterns("/js/*");
}
}
Step4: 前端发送用户名验证码,后端验证通过,向前端发送token:
// 情况3:查询到一个用户时
try {
User userDB = userListDB.get(0);
// 用户输入的密码和盐进行加密
String md5Pwd = SecureUtil.md5(user.getPassword() + userDB.getSalt());
// 密码不一致,返回:用户名或密码错误
if ( !userDB.getPassword().equals(md5Pwd) ) {
map.put("state",false);
map.put("msg","提示:用户名或密码错误");
return map;
}
Map<String,String> payload = new HashMap<>();
payload.put("confirmCode",userDB.getConfirmCode());
payload.put("name",userDB.getName());
// 生成JWT令牌
String token = JWTUtils.getToken(payload);
map.put("state",true);
map.put("msg","认证成功");
map.put("token",token); // 响应token
} catch (Exception e) {
map.put("state",false);
map.put("msg",e.getMessage());
}
return map;
Step5: 前端获得token,存储、及之后前端访问后端接口时携带token的方法,参考axios发送post和get请求的时候,带请求头的写法
localStorage.setItem("token",res.data.token);
var token = localStorage.getItem("token");
that = this;
axios.get("/salieri/item/getItemById?itemid=" + this.itemid,
{
headers:{
'token':token
}
}
).then((res) => {
that.item = res.data;
})
var token = localStorage.getItem("token");
var that = this;
var itemid = that.itemid;
axios.post("/salieri/order/alipay/callback",
{itemid: itemid},
{
headers:{
'token':token
}
}
).then(function (res) {
console.log("res======>",res);
if (res.data.code == 20000) {
alert("该商品已被购买");
} else {
alert("该商品未被购买");
}
})
// 清空浏览器中token的方法
localStorage.removeItem("token");