apache旗下的一个开源框架,实现用户身份认证,权限授权,加密,会话管理等功能
内容均转载自Shiro和JWT
MD5加密
概述
MD5消息摘要算法,属Hash算法一类。MD5算法对输入任意长度的消息进行运行,产生一个128位的消息摘要(32位的数字字母混合码)。 也就是0123456789ABCDEF构成的混合数字码
特点
不可逆:相同数据的MD5值肯定一样,不同数据的MD5值不一样。
压缩性:任意长度的数据,算出的MD5值长度都是固定的
弱抗碰撞:已知原数据和MD5值,想找到一个具有相同MD5值是非常困难的。
强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
为了让MD5码更加安全,涌现了很多其他方法,如加盐。 盐要足够长足够乱 得到的MD5码就很难查到。
用途
- 防止被篡改
A发送数据,通过MD5得到输出A,在B接受数据时,也计算一次MD5得到输出B,如果A=B,就相当于没有被篡改 - 防止直接看到明文
网站数据库存储用户密码都是存储的密码的MD5值,这样就算黑客得到MD5值,也无法知道密码(不可逆性) - 数字签名
就比如可以在用户登陆的时候,将用户输入的密码与系统数据库中的salt进行MD5,此时生成的字符串与系统中注册生成的MD5值是否一致,判断是否为合法登陆。这样既保证了系统不会知道用户密码明文的情况就可以确定用户登陆的合法性,又可以恶意者增加密码破解的难度
身份认证
介绍
realm:相当于datasource数据源,securityManager进行安全认证需要通过Realm获取数据,但realm并不是只从数据源中获取数据,也包含了认证授权校验相关代码。
流程:
开发
2.1 依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
2.2 创建resource/shrio,ini配置文件
[users]
xiaochen=123
zhangsan=456
2.3 开发认证
public class TestAuthenticator {
public static void main(String[] args) {
//1.创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2.给安全管理器设置realm
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//3.SecurityUtils 给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//4.关键对象 subject 主体
Subject subject = SecurityUtils.getSubject();
//5.创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen","123");
try{
System.out.println("认证状态: "+ subject.isAuthenticated());
subject.login(token);//用户认证
System.out.println("认证状态: "+ subject.isAuthenticated());
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("认证失败: 用户名不存在~");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败: 密码错误~");
}
}
}
但上述的是从shiro.ini配置文件获取用户信息,实际开发中一般都将用户信息存储在数据库之中,因此需要自定义realm
认证使用的是SimpleAccountRealm,源码如下
public class SimpleAccountRealm extends AuthorizingRealm {
//.......省略
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}
}
因此在自定义realm中实现认证和授权两个方法(现在只实现了认证)
package com.baizhi.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* 自定义realm实现 将认证|授权数据的来源转为数据库的实现
*/
public class CustomerRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("==================");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//在token中获取用户名
String principal = (String) token.getPrincipal();
System.out.println(principal);
//根据身份信息使用jdbc mybatis查询相关数据库
if("xiaochen".equals(principal)){
//参数1:返回数据库中正确的用户名 //参数2:返回数据库中正确密码 //参数3:提供当前realm的名字 this.getName();
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
2.4 使用MD5和Salt
Salt:由系统随机生成,并且只有系统知道,这样即便两个用户使用了同一个密码,由于系统给他们生成的salt的值不同,散列值也是不同的,这样即便黑客能够获取到密码,也需要获取到系统中salt才能成功登陆
加Salt可以一定程度上解决这一问题。所谓加Salt,就是加点“佐料”。其基本想法是这样的——当用户首次提供密码时(通常是注册时),由系统自动往这个密码里撒一些“佐料”,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后散列,再比较散列值,已确定密码是否正确。
实际应用:是将盐和散列后的值存在数据库中,自动realm从数据库取出盐和加密后的值由shiro完成密码校验。
校验流程:
2.5 实现工具类
package com.lx.shopping.utils;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
/*这是一个非常好用的使用MD5+salt加密的工具类。使用这个工具类,非常简单,
从前台拿到密码passwd,直接HexUtil.getEncryptedPwd(passwd)就可以返回一个长度为56的字符串,
可以用来保存到数据库中,相反,登录的时候,因为MD5加密是不可逆的运算,只能拿用户输入的密码走一遍MD5+salt加密之后,
跟数据库中的passwd比较,看是否一致,一致时密码相同,登录成功,通过调用HexUtil.validPasswd(String passwd,String dbPasswd)方法,
就可以了,不用再做其他事。*/
public class MD5Util {
private final static String HEX_NUMS_STR = "0123456789ABCDEF";
private final static Integer SALT_LENGTH = 12;
/**
* 将16进制字符串转换成数组
*
* @return byte[]
* @author jacob
* */
public static byte[] hexStringToByte(String hex) {
/* len为什么是hex.length() / 2 ?
* 首先,hex是一个字符串,里面的内容是像16进制那样的char数组
* 用2个16进制数字可以表示1个byte,所以要求得这些char[]可以转化成什么样的byte[],首先可以确定的就是长度为这个char[]的一半
*/
int len = (hex.length() / 2);
byte[] result = new byte[len];
char[] hexChars = hex.toCharArray();
for (int i = 0; i < len; i++) {
int pos = i * 2;
result[i] = (byte) (HEX_NUMS_STR.indexOf(hexChars[pos]) << 4 | HEX_NUMS_STR
.indexOf(hexChars[pos + 1]));
}
return result;
}
/**
* 将数组转换成16进制字符串
*
* @return String
* @author jacob
*
* */
public static String byteToHexString(byte[] salt){
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < salt.length; i++) {
String hex = Integer.toHexString(salt[i] & 0xFF);
if(hex.length() == 1){
hex = '0' + hex;
}
hexString.append(hex.toUpperCase());
}
return hexString.toString();
}
/**
* 密码验证
* @param passwd 用户输入密码
* @param dbPasswd 数据库保存的密码
* @return
* @throws NoSuchAlgorithmException
* @throws UnsupportedEncodingException
*/
public static boolean validPasswd(String passwd, String dbPasswd)
throws NoSuchAlgorithmException, UnsupportedEncodingException{
byte[] pwIndb = hexStringToByte(dbPasswd);
//定义salt
byte[] salt = new byte[SALT_LENGTH];
System.arraycopy(pwIndb, 0, salt, 0, SALT_LENGTH);
//创建消息摘要对象
MessageDigest md = MessageDigest.getInstance("MD5");
//将盐数据传入消息摘要对象
md.update(salt);
md.update(passwd.getBytes("UTF-8"));
byte[] digest = md.digest();
//声明一个对象接收数据库中的口令消息摘要
byte[] digestIndb = new byte[pwIndb.length - SALT_LENGTH];
//获得数据库中口令的摘要
System.arraycopy(pwIndb, SALT_LENGTH, digestIndb, 0,digestIndb.length);
//比较根据输入口令生成的消息摘要和数据库中的口令摘要是否相同
if(Arrays.equals(digest, digestIndb)){
//口令匹配相同
return true;
}else{
return false;
}
}
/**
* 获得md5之后的16进制字符
* @param passwd 用户输入密码字符
* @return String md5加密后密码字符
* @throws NoSuchAlgorithmException
* @throws UnsupportedEncodingException
*/
public static String getEncryptedPwd(String passwd)
throws NoSuchAlgorithmException, UnsupportedEncodingException{
//拿到一个随机数组,作为盐
byte[] pwd = null;
SecureRandom sc= new SecureRandom();
byte[] salt = new byte[SALT_LENGTH];
sc.nextBytes(salt);
//声明摘要对象,并生成
MessageDigest md = MessageDigest.getInstance("MD5");
//计算MD5函数
md.update(salt);
//passwd.getBytes("UTF-8")将输入密码变成byte数组,即将某个数装换成一个16进制数
md.update(passwd.getBytes("UTF-8"));
//计算后获得字节数组,这就是那128位了即16个元素
byte[] digest = md.digest();
pwd = new byte[salt.length + digest.length];
System.arraycopy(salt, 0, pwd, 0, SALT_LENGTH);
System.arraycopy(digest, 0, pwd, SALT_LENGTH, digest.length);
return byteToHexString(pwd);
}
}
授权
流程:
介绍
权限字符串的规则是:资源标识符:操作:资源实例标识符。
意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。
例子:
用户创建权限:user:create,或user:create:*
用户修改实例001的权限:user:update:001
用户对001有所有权限:user:*:001
使用
注解
@RequiresRoles("admin")
public void hello() {
//有权限
}
开发
package com.baizhi.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
/**
* 使用自定义realm 加入md5 + salt +hash
*/
public class CustomerMd5Realm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("身份信息: "+primaryPrincipal);
//根据身份信息 用户名 获取当前用户的角色信息,以及权限信息 xiaochen admin user
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//将数据库中查询角色信息赋值给权限对象
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");
//将数据库中查询权限信息赋值个权限对象
simpleAuthorizationInfo.addStringPermission("user:*:01");
simpleAuthorizationInfo.addStringPermission("product:create");
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取身份信息
String principal = (String) token.getPrincipal();
//根据用户名查询数据库
if ("xiaochen".equals(principal)) {
//参数1: 数据库用户名 参数2:数据库md5+salt之后的密码 参数3:注册时的随机盐 参数4:realm的名字
return new SimpleAuthenticationInfo(principal,
"e4f9bf3e0c58f045e62c23c533fcf633",
ByteSource.Util.bytes("X0*7ps"),
this.getName());
}
return null;
}
}
整合SpringBoot
介绍
流程
依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
开发
这里就是流程request会进入shiro过滤器所需要创建的内容
具体内容:
- 配置shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager){
//创建shiro的filter
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//注入安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
- 配置WebSecurityManager
@Bean
public DefaultWebSecurityManager getSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
- 配置自定义realm(如上述,授权和认证)
//创建自定义realm
@Bean
public Realm getRealm(){
return new CustomerRealm();
}
CustomerRealm(自定义realm类)
public class CustomerRealm extends AuthorizingRealm {
//处理授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//处理认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws
AuthenticationException {
return null;
}
}
过滤器
shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限
这里不列举了
md5 salt的注册实现
1.引入依赖
<!--mybatis相关依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
2.配置application.properties
server.port=8888
server.servlet.context-path=/shiro
spring.application.name=shiro
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
#新增配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.type-aliases-package=com.baizhi.springboot_jsp_shiro.entity
mybatis.mapper-locations=classpath:com/baizhi/mapper/*.xml
3.创建pojo、数据库、操作pojo接口(mybatis)、service接口
3.1 user类
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String id;
private String username;
private String password;
private String salt;
}
3.2 数据库
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`username` varchar(40) DEFAULT NULL,
`password` varchar(40) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
3.3 mapper
@Mapper
public interface UserDAO {
void save(User user);
}
mepper的配置
<insert id="save" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into t_user values(#{id},#{username},#{password},#{salt})
</insert>
3.4 service
public interface UserService {
//注册用户方法
void register(User user);
}
4.salt工具类
系统在注册的时候随机生成盐
public class SaltUtils {
/**
* 生成salt的静态方法
* @param n
* @return
*/
public static String getSalt(int n){
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
char aChar = chars[new Random().nextInt(chars.length)];
sb.append(aChar);
}
return sb.toString();
}
}
5.service实现类
这里是注册的功能,首次注册要生成一个盐+md5值,保存在系统的数据库中
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
public void register(User user) {
//处理业务调用dao
//1.生成随机盐
String salt = SaltUtils.getSalt(8);
//2.将随机盐保存到数据
user.setSalt(salt);
//3.明文密码进行md5 + salt + hash散列
Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
user.setPassword(md5Hash.toHex());
userDAO.save(user);
}
}
6.controller
@Controller
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
/**
* 用户注册
*/
@RequestMapping("register")
public String register(User user) {
try {
userService.register(user);
return "redirect:/login.jsp";
}catch (Exception e){
e.printStackTrace();
return "redirect:/register.jsp";
}
}
}
注册成功结果
md5 salt的授权+数据库
1.mapper添加查询功能
@Mapper
public interface UserDAO {
void save(User user);
//根据身份信息认证的方法
User findByUserName(String username);
}
<select id="findByUserName" parameterType="String" resultType="User">
select id,username,password,salt from t_user
where username = #{username}
</select>
2.实现userservice接口
public interface UserService {
//注册用户方法
void register(User user);
//根据用户名查询业务的方法
User findByUserName(String username);
}
@Service("userService")
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
public User findByUserName(String username) {
return userDAO.findByUserName(username);
}
}
3.创建从工厂获取bean对象的工具类,目的就是获取自定义realm,获取自定义的就可以获取到数据库中存放的用户信息
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
//根据bean名字获取工厂中指定bean 对象
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
4.修改自定义realm
public class CustomerRealm extends AuthorizingRealm{
...
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("==========================");
//根据身份信息
String principal = (String) token.getPrincipal();
//在工厂中获取service对象
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
//根据身份信息查询
User user = userService.findByUserName(principal);
if(!ObjectUtils.isEmpty(user)){
//返回数据库信息
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),this.getName());
}
return null;
}
}
5.修改自定义ShiroConfig中的自定义realm的使用
@Configuration
public class ShiroConfig{
...
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//设置hashed凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置md5加密
credentialsMatcher.setHashAlgorithmName("md5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
}
6.授权用户权限
@RequestMapping("save")
public String save(){
System.out.println("进入方法");
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//代码方式
if (subject.hasRole("admin")) {
System.out.println("保存订单!");
}else{
System.out.println("无权访问!");
}
//基于权限字符串
//....
return "redirect:/index.jsp";
}
或者
@RequiresRoles(value={"admin","user"})//用来判断角色 同时具有 admin user
@RequiresPermissions("user:update:01") //用来判断权限字符串
@RequestMapping("save")
public String save(){
System.out.println("进入方法");
return "redirect:/index.jsp";
}
7.那么授权就需要持久化权限,用户并不是只有这一次权限
因此权限表+用户表建立一个用户权限表
7.1 创建表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_pers
-- ----------------------------
DROP TABLE IF EXISTS `t_pers`;
CREATE TABLE `t_pers` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`name` varchar(80) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`name` varchar(60) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
`id` int(6) NOT NULL,
`roleid` int(6) DEFAULT NULL,
`permsid` int(6) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`username` varchar(40) DEFAULT NULL,
`password` varchar(40) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`id` int(6) NOT NULL,
`userid` int(6) DEFAULT NULL,
`roleid` int(6) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
7.2 dao接口,配置dao的操作
略
8.重写自定义realm中授权相关代码
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("调用授权验证: "+primaryPrincipal);
//根据主身份信息获取角色 和 权限信息
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
User user = userService.findRolesByUserName(primaryPrincipal);
//授权角色信息
if(!CollectionUtils.isEmpty(user.getRoles())){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role->{
simpleAuthorizationInfo.addRole(role.getName());
//权限信息
List<Perms> perms = userService.findPermsByRoleId(role.getId());
if(!CollectionUtils.isEmpty(perms)){
perms.forEach(perm->{
simpleAuthorizationInfo.addStringPermission(perm.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
}
CacheManager
SecurityManager里的CacheManager就可以用来减轻DB的访问压力,从而提高系统的查询
流程
访问发现在缓存中没有,就往数据库中查询,得到结果后并将结果返回给缓存,缓存保存,返回给用户。(缓存还可以设置期限)
引入依赖
<!--引入shiro和ehcache-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
redis作为缓存数据库
<!--redis整合springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1.在自定义realm中添加缓存CacheManager
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密算法为md5
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
//开启缓存管理
customerRealm.setCacheManager(new RedisCacheManager());
customerRealm.setCachingEnabled(true);//开启全局缓存
customerRealm.setAuthenticationCachingEnabled(true);//认证认证缓存
customerRealm.setAuthenticationCacheName("authenticationCache");
customerRealm.setAuthorizationCachingEnabled(true);//开启授权缓存
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
JWT
介绍
JWT全称是:JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,可以将信息作为JSON对象数据安全传输,同时完成数据加密、签名等处理。
作用
- 授权
JWT广泛运用在单点登录上。 - 信息交换
token可以进行签名,这样就能验证内容是否遭到篡改。
session
session的来源:**http是一种无状态的协议,**第一次向应用提交了用户名和密码进行用户验证后,成功了,那么在下一次请求时,用户还要进行认证(无状态),因此就为了让应用能够识别这是哪个用户发出的请求,就在服务器中存储一份用户登录的信息session,同时这份登录信息会传递给浏览器,告诉保存为cookie,以便下次请求发送给应用,用cookie去查询session,这样下次访问应用就能够识别是哪个用户。
遗留问题:
- session是保存在内存中,随着用户的增多,那么服务端的开销就很大。
- 保存在内存中,那么就意味着不能进行分布式应用,获取不到,不能实现负载均衡,限制了应用的扩展能力
- 如果cookie被恶意获取了怎么办?会出现跨站请求伪造攻击CSRF
- 前后端分离
因此引出了基于JWT的认证
jwt认证流程
- 前端发出HTTP post请求中有用户名&密码,建议SSL加密传输(HTTPS协议),避免敏感信息被嗅探
- 用户名&密码识别成功,将信息进行Base64编码拼接形成JWT(Token),类似于一个字符串token head.payload.singurater
- Token返回给前端,前端将token保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
- 此时前端storage中有了Token,这样就在每次请求放在HTTP Header的Authorization位置上,这样就可以解决XSS、CSRF
- 后端收到请求时检查Token是否过期、是否接收方是自己,如果正确就执行业务逻辑,否则返回错误页面
jwt优点
- 简洁
可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。 - 适合分布式微服务
jwt和session是不一样的,jwt存储在客户端上,session存储咋服务器上。服务器断电后session就没了,而jwt因为存储在客户端,所以就不会影响,只要jwt不过期,就可以继续使用。 - 跨语言
因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
jwt结构
token string ====> header.payload.singnature token
# 令牌组成
- 1.标头(Header)
- 2.有效载荷(Payload)
- 3.签名(Signature)
- 因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz Header.Payload.Signature
内容:
-
Header = token类型 + 所使用的签名算法,HMAC SHA256或RSA
{ "alg": "HS256", "typ": "JWT" }
-
Payload = 实体内容
{ "sub": "1234567890", "name": "John Doe", "admin": true }
-
Signature = 使用一个密钥对base64编码后的header和payload进行签名,防止内容被篡改
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret);
注意:base64是可逆的。
引出的问题,那么base64可逆的话,恶意者获取到了信息是有可能被获取到的,那么信息就会被暴露?
的确,所以Token中,不应该在Payload中加入任何敏感数据,在这里我们只是传输了User Id,而并不是用户密码。
因此JWT适合向Web应用传递一些非敏感信息,实现单点登录
开发
引入依赖
<!--引入jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
1.生成token(三部分)
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 90);
//生成令牌
String token = JWT.create()
.withClaim("username", "张三")//设置自定义用户名
.withExpiresAt(instance.getTime())//设置过期时间
.sign(Algorithm.HMAC256("token!Q2W#E$RW"));//设置签名 保密 复杂
//输出令牌
System.out.println(token);
生成结果
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsicGhvbmUiLCIxNDMyMzIzNDEzNCJdLCJleHAiOjE1OTU3Mzk0NDIsInVzZXJuYW1lIjoi5byg5LiJIn0.aHmE3RNqvAjFr_dvyn_sD2VJ46P7EGiS5OBMO_TI5jg
2.解析token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!Q2W#E$RW")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
System.out.println("用户名: " + decodedJWT.getClaim("username").asString()); 、// 存的是时候是什么类型,取得时候就是什么类型,否则取不到值。
System.out.println("过期时间: "+decodedJWT.getExpiresAt());
工具类
public class JWTUtils {
private static String TOKEN = "token!Q@W3e4r";
/**
* 生成token
* @param map //传入payload,非敏感信息
* @return 返回token
*/
public static String getToken(Map<String,String> map){
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,7);
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(TOKEN));
}
/**
* 验证token
* @param token
* @return
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token); // 如果验证通过,则不会把报错,否则会报错
}
/**
* 获取token中payload
* @param token
* @return
*/
public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
}
使用例子:
-
user实体
@Data @Accessors(chain=true) public class User { private String id; private String name; private String password; }
-
mapper、配置mapper、service、serviceImpl,数据库表创建
略 -
controller业务逻辑
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/login")
public Map<String,Object> login(User user) {
Map<String,Object> result = new HashMap<>();
log.info("用户名: [{}]", user.getName());
log.info("密码: [{}]", user.getPassword());
try {
User userDB = userService.login(user);
Map<String, String> map = new HashMap<>();//用来存放payload
map.put("id",userDB.getId());
map.put("username", userDB.getName());
String token = JWTUtils.getToken(map);
result.put("state",true);
result.put("msg","登录成功!!!");
result.put("token",token); //成功返回token信息
} catch (Exception e) {
e.printStackTrace();
result.put("state","false");
result.put("msg",e.getMessage());
}
return result;
}
}