目录
一、shiro框架的配置
拦截器配置
package com.itqq.interceptor;
import com.itqq.shiro.UUIDToken;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Created with IntelliJ IDEA.
*
* @Author: syq
* @Description:使用过滤器用户在进行权限验证时必须要先认证,一个个加太麻烦
* 所以在拦截器中统一进行认证
*
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
String token = request.getHeader("token");
// 使用Shiro框架的SecurityUtils.getSubject()方法获取当前的Subject(即用户身份)
Subject subject = SecurityUtils.getSubject();
// 将token传给UUIDRealm进行认证
subject.login(new UUIDToken(token));
}catch (Exception e){
e.printStackTrace();
}
return true;
}
}
package com.itqq.config;
import com.itqq.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Created with IntelliJ IDEA.
*
* @Author: syq
* @Description:
*/
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 将loginInterceptor这个拦截器注册到Spring Boot中,并排除对/employee/login(登录)这个路径的拦截
registry.addInterceptor(loginInterceptor)
.excludePathPatterns("/employee/login")
.excludePathPatterns("/employee/captcha")
.excludePathPatterns("/employee/sendSms");
}
}
使用shiro框架必须的配置
package com.itqq.config;
import com.itqq.realm.MyRealm;
import com.itqq.realm.SmsRealm;
import com.itqq.realm.UUIDRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import java.util.ArrayList;
import java.util.List;
/**
* Created with IntelliJ IDEA.
*
* @Author: syq
* @Description:shiro整合Spring的配置文件
*/
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm myRealm;
@Autowired
private UUIDRealm uuidRealm;
@Autowired
private SmsRealm smsRealm;
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
aasa.setSecurityManager(securityManager());
return new AuthorizationAttributeSourceAdvisor();
}
// 设置安全管理器securityManager
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List<Realm> realms = new ArrayList<>();
realms.add(myRealm);
realms.add(uuidRealm);
realms.add(smsRealm);
securityManager.setRealms(realms);
return securityManager;
}
// 设置filter
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager( securityManager());
shiroFilter.setLoginUrl("/anon");
shiroFilter.setUnauthorizedUrl("/anon");
return shiroFilter;
}
// @Bean
// public MyRealm myRealm(){
// return new MyRealm();
// }
}
二、图形验证码接口(游客接口)
2.1 pojo
package com.itqq.pojo;
import lombok.Data;
import java.io.Serializable;
/**
* Created with IntelliJ IDEA.
*
* @Author: syq
* @Description:验证码封装对象
*/
@Data
public class Captcha implements Serializable {
private static final long serialVersionUID = 1495038284838382L;
// 图型验证码供前端使用
private String base64Str;
// 唯一标识符--获取验证码
private String client;
// 用户传入的验证码(值)
private String captcha;
// 用户接收的手机号码
private String phone;
}
2.2 controller
// 1. 图形验证码接口
// 请求地址:http://*****/user/captcha
// 请求方式:GET
// 请求参数:无
// 返回值: 图片--图片验证码,client
// 获取图形验证码接口
@GetMapping("captcha")
public Result captcha() {
Captcha validateCode = employeeService.getValidateCode();
log.info(validateCode.toString());
return Result.success(employeeService.getValidateCode());
}
2.3 service和serviceImpl
Captcha getValidateCode();
public Captcha getValidateCode() {
ValidateCodeUtil.Validate validate = ValidateCodeUtil.getRandomCode();
Captcha captcha = new Captcha();
// 将生成的base64格式的图形验证码放到captcha对象上
captcha.setBase64Str(validate.getBase64Str());
// 唯一标识符,这里使用uuid
captcha.setClient("AHCTPAC"+ UUID.randomUUID().toString().replace("-","").toUpperCase());
// 将标识符作为key,验证码值validate.getValue作为value(因为验证码是随机生成的,并发量大的话会重复),存入redis中
// redis String类型
redisTemplate.opsForValue().set(captcha.getClient(), validate.getValue());
redisTemplate.expire(captcha.getClient(),5, TimeUnit.MINUTES);//10分钟过期时间
log.info("验证码:{}",validate.getValue());
// 返回图形验证码用于前端展示
return captcha;
}
2.4 图形验证码工具类
package com.itqq.utils;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Base64;
import java.util.Random;
import javax.imageio.ImageIO;
public class ValidateCodeUtil {
private static Validate validate = null; //验证码类,用于最后返回此对象,包含验证码图片base64和真值
private static Random random = new Random(); //随机类,用于生成随机参数
private static String randString = "0123456789";//随机生成字符串的取值范围
private static int width = 100; //图片宽度
private static int height = 40; //图片高度
private static int StringNum = 6; //字符的数量
private static int lineSize = 40; //干扰线数量
//将构造函数私有化 禁止new创建
private ValidateCodeUtil() {
super();
}
/**
* 获取随机字符,并返回字符的String格式
* @param index (指定位置)
* @return
*/
private static String getRandomChar(int index) {
//获取指定位置index的字符,并转换成字符串表示形式
return String.valueOf(randString.charAt(index));
}
/**
* 获取随机指定区间的随机数
* @param min (指定最小数)
* @param max (指定最大数)
* @return
*/
private static int getRandomNum(int min,int max) {
return (int)(Math.random()* (max-min+1))+min;
}
/**
* 获得字体
* @return
*/
private static Font getFont() {
return new Font("Fixedsys", Font.CENTER_BASELINE, 25); //名称、样式、磅值
}
/**
* 获得颜色
*/
private static Color getRandColor(int frontColor, int backColor) {
if(frontColor > 255)
frontColor = 255;
if(backColor > 255)
backColor = 255;
int red = frontColor + random.nextInt(backColor - frontColor - 16);
int green = frontColor + random.nextInt(backColor - frontColor -14);
int blue = frontColor + random.nextInt(backColor - frontColor -18);
return new Color(red, green, blue);
}
/**
* 绘制字符串,返回绘制的字符串
* @param g
* @param randomString
* @param i
* @return
*/
private static String drawString(Graphics g, String randomString, int i) {
Graphics2D g2d = (Graphics2D) g;
g2d.setFont(getFont()); //设置字体
g2d.setColor(new Color(random.nextFloat(), random.nextFloat(), random.nextFloat()));//设置颜色
String randChar = String.valueOf(getRandomChar(random.nextInt(randString.length())));
randomString += randChar; //组装
int rot = getRandomNum(5,10);
g2d.translate(random.nextInt(3), random.nextInt(3));
g2d.rotate(rot * Math.PI / 180);
g2d.drawString(randChar, 13*i, 20);
g2d.rotate(-rot * Math.PI / 180);
return randomString;
}
/**
* 绘制干扰线
* @param g
*/
private static void drawLine(Graphics g) {
//起点(x,y) 偏移量x1、y1
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(13);
int yl = random.nextInt(15);
g.setColor(new Color(random.nextFloat(), random.nextFloat(), random.nextFloat()));
g.drawLine(x, y, x + xl, y + yl);
}
/**
* @Description: 生成Base64图片验证码
*/
public static Validate getRandomCode() {
validate = validate==null?new Validate():validate;
// BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
Graphics g = image.getGraphics();// 获得BufferedImage对象的Graphics对象
g.fillRect(0, 0, width, height);//填充矩形
g.setFont(new Font("Times New Roman", Font.ROMAN_BASELINE, 18));//设置字体
g.setColor(getRandColor(110, 133));//设置颜色
//绘制干扰线
for(int i = 0; i <= lineSize; i++) {
drawLine(g);
}
//绘制字符
String randomString = "";
for(int i = 1; i <= StringNum; i++) {
randomString = drawString(g, randomString, i);
validate.setValue(randomString);
}
g.dispose();//释放绘图资源
ByteArrayOutputStream bs = null;
try {
bs = new ByteArrayOutputStream();
ImageIO.write(image, "png", bs);//将绘制得图片输出到流
String imgsrc = Base64.getEncoder().encodeToString(bs.toByteArray());
validate.setBase64Str(imgsrc);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bs.close();
} catch (IOException e) {
e.printStackTrace();
}finally{
bs = null;
}
}
return validate;
}
public static class Validate implements Serializable{
private static final long serialVersionUID = 1L;
private String Base64Str; //Base64 值
private String value; //验证码值
public String getBase64Str() {
return "data:image/jpeg;base64,"+Base64Str;
}
public void setBase64Str(String base64Str) {
Base64Str = base64Str;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
三、短信发送接口(游客接口)
pojo用的也是Captcha,短信发送我用的是阿里云的短信服务
阿里云申请完短信服务后,牢记id和secret,并写入配置文件如:
aliyun: ACCESS_KEY_ID: ************ ACCESS_KEY_SECRET: ***************
3.1 阿里短信发送工具类
SMSUtils
使用时只需要把senderSms方法的setSignName和setTemplateCode改成自己账号下的签名和模板号,就可使用
package com.itqq.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* Created with IntelliJ IDEA.
*
* @Author: syq
* @Description:
*/
@Component
public class SMSUtils {
@Value("${aliyun.ACCESS_KEY_ID}")
private String ACCESS_KEY_ID;
@Value("${aliyun.ACCESS_KEY_SECRET}")
private String ACCESS_KEY_SECRET;
/**
* 使用AK&SK初始化账号Client
* @param accessKeyId
* @param accessKeySecret
* @return Client
* @throws Exception
*/
private com.aliyun.dysmsapi20170525.Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
// 必填,您的 AccessKey ID
.setAccessKeyId(accessKeyId)
// 必填,您的 AccessKey Secret
.setAccessKeySecret(accessKeySecret);
// 访问的域名
config.endpoint = "dysmsapi.aliyuncs.com";
return new com.aliyun.dysmsapi20170525.Client(config);
}
/**
* 使用STS鉴权方式初始化账号Client,推荐此方式。
* @param accessKeyId
* @param accessKeySecret
* @param securityToken
* @return Client
* @throws Exception
*/
private com.aliyun.dysmsapi20170525.Client createClientWithSTS(String accessKeyId, String accessKeySecret, String securityToken) throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
// 必填,您的 AccessKey ID
.setAccessKeyId(accessKeyId)
// 必填,您的 AccessKey Secret
.setAccessKeySecret(accessKeySecret)
// 必填,您的 Security Token
.setSecurityToken(securityToken)
// 必填,表明使用 STS 方式
.setType("sts");
// 访问的域名
config.endpoint = "dysmsapi.aliyuncs.com";
return new com.aliyun.dysmsapi20170525.Client(config);
}
public void senderSms(String phone,String code) throws Exception {
com.aliyun.dysmsapi20170525.Client client = createClient(ACCESS_KEY_ID, ACCESS_KEY_SECRET);
com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest()
.setSignName("基尼太美")
.setTemplateCode("SMS_*********")
.setPhoneNumbers(phone)
.setTemplateParam("{\"code\":\"" + code + "\"}");
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
com.aliyun.dysmsapi20170525.models.SendSmsResponse resp = client.sendSmsWithOptions(sendSmsRequest, runtime);
com.aliyun.teaconsole.Client.log(com.aliyun.teautil.Common.toJSONString(resp));
}
}
3.2 controller
/**
*发送短信验证码接口
* @param "phone",client,captcha
* @return
*/
@PostMapping("sendSms")
public Result sendSms(@RequestBody Captcha captcha) throws Exception {
return employeeService.sendSms(captcha) ? Result.success("发送成功") : Result.failed("发送失败");
}
3.3 service和serviceImpl
boolean sendSms(Captcha captcha) throws Exception;
// 发送短信验证码接口
@Override
public boolean sendSms(Captcha captcha) throws Exception {
// 1.判断图形验证码是否正确
String value = (String) redisTemplate.opsForValue().get(captcha.getClient());
if (!StringUtils.hasText(value)){
throw new RuntimeException("图形验证码已过期");
}
if (!value.equals(captcha.getCaptcha())){
throw new RuntimeException("图形验证码错误");
}
// 判断同一手机号短时间内是否重复发送验证码
if (redisTemplate.hasKey("expire"+captcha.getPhone())){
throw new RuntimeException("一分钟内不能重复发送验证码");
}
// 2.发送验证码
String code = randomUtil.randomCode();
// 有异常需要抛出
smsUtils.senderSms(captcha.getPhone()+"",code);
// 3.将短信验证码存入redis中 phone为key,验证码为value
redisTemplate.opsForValue().set("SMS"+captcha.getPhone(),code);
// 4.短信验证码过期时间
redisTemplate.expire("SMS"+captcha.getPhone(),5, TimeUnit.MINUTES);
// 5.实现同一手机号一分钟内不能重复发送短信验证码
redisTemplate.opsForValue().set("expire"+captcha.getPhone(),"0");
redisTemplate.expire("expire"+captcha.getPhone(),1, TimeUnit.MINUTES);
return true;
}
@Component
public class RandomUtil {
public String randomCode(){
String code = "";
for (int i=0;i<6;i++){
code += (int)(Math.random()*10);
}
return code;
}
}
四、登录接口(游客接口)
4.1 controller
/**
* 登录接口
* @param smsToken
* @return
*/
@PostMapping("login")
public Result login(@RequestBody SmsToken smsToken) {
// 此token是前端传来的登录信息,这里是phone和短信验证码
SecurityUtils.getSubject().login(smsToken);
// 登录成功后返回token
String tokenString = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(tokenString, 1);
return Result.success(tokenString);
}
4.2 Shiro-SmsToken
package com.itqq.shiro;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.shiro.authc.AuthenticationToken;
import java.io.Serializable;
/**
* Created with IntelliJ IDEA.
*
* @Author: syq
* @Description:
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsToken implements AuthenticationToken, Serializable {
private static final long serialVersionUID = 8885806135738587998L;
private Long phone;
private String code;
@Override
public Object getPrincipal() {
return phone;
}
@Override
public Object getCredentials() {
return code;
}
}
4.3 realm
登录时需要对用户输入的手机号进行系统验证,如果不存在则不能登录,也可以不存在直接使用该账号注册,本文没有拓展注册功能,直接输出账号不存在。判断手机号是否纯在需要查数据库。
package com.itqq.realm;
import com.itqq.pojo.Employee;
import com.itqq.service.EmployeeService;
import com.itqq.shiro.SmsToken;
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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* Created with IntelliJ IDEA.
*
* @Author: syq
* @Description:
*/
@Component
public class SmsRealm extends AuthorizingRealm {
@Autowired
private EmployeeService employeeService;
@Autowired
private RedisTemplate redisTemplate;
public boolean supports(AuthenticationToken token) {
return token instanceof SmsToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
Long phone = (Long) token.getPrincipal();
String captcha = (String) token.getCredentials();
if(phone == null){
throw new AuthenticationException("手机号不能为空");
}
if(captcha == null){
throw new AuthenticationException("验证码不能为空");
}
// 判断手机验证码是否过期
String captcha1 = (String)redisTemplate.opsForValue().get("SMS" + phone);
if(!StringUtils.hasText(captcha1)){
throw new AuthenticationException("短信验证码已过期");
}
Employee employee = employeeService.findEmployeeByPhone(phone);
if(employee == null){
throw new AuthenticationException("手机号不存在");
}
// 验证短信验证码是否正确,shiro自带的框架验证
return new SimpleAuthenticationInfo(employee.getId(),captcha1,getName());
}
}
五、登录后的权限验证
5.1 Shiro-UUIDToken
package com.itqq.shiro;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.shiro.authc.AuthenticationToken;
import java.io.Serializable;
/**
* Created with IntelliJ IDEA.
*
* @Author: syq
* @Description:
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UUIDToken implements AuthenticationToken, Serializable {
private static final long serialVersionUID = 8884806189738587998L;
private String uuid;
@Override
public Object getPrincipal() {
return uuid;
}
@Override
public Object getCredentials() {
return uuid;
}
}
5..2 realm
package com.itqq.realm;
import com.itqq.service.PermissionService;
import com.itqq.service.RoleService;
import com.itqq.shiro.UUIDToken;
import lombok.extern.slf4j.Slf4j;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* Created with IntelliJ IDEA.
*
* @Author:syq
* @Description:
*/
@Component
@Slf4j
public class UUIDRealm extends AuthorizingRealm {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
//用于判断传入的AuthenticationToken对象是否是UUIDToken的实例。如果是,则返回true,表示支持该令牌;否则返回false,表示不支持该令牌。
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UUIDToken;
}
// 该函数调用了roleService.findRoleNameByEmployeeId(id)方法,
// 将返回值添加到info对象的roles属性中。
// findRoleNameByEmployeeId方法根据员工ID查找对应的职位名称
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
Integer id = (Integer) principalCollection.getPrimaryPrincipal();
log.info("id:{}",id);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roleService.findRoleNameByEmployeeId(id));
info.addStringPermissions(permissionService.findPermissionNameByEmployeeId(id));
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从令牌中提取UUID作为凭证
String uuid = (String) token.getCredentials();
if(!StringUtils.hasText(uuid)){
throw new RuntimeException("token不能为空");
}
// 从resdis中获取员工id
Integer id = (Integer)redisTemplate.opsForValue().get(uuid);
if(id == null){
throw new RuntimeException("token已经失效");
}
// 构建认证信息对象,包含用户ID、UUID和名称
// 将员工id,uuid(生成的),名称送去校验
return new SimpleAuthenticationInfo(id,uuid,getName());
}
}
itqq