前期概要
Shiro 是一个开源的 Java 安全框架,由 Apache 开发和维护。Shiro 可以帮助开发人员快速实现安全特性,包括身份认证、授权、加密和会话管理等。Shiro 的目标是简化 Java 安全编程,并提供更好的开发体验。
Shiro 的主要特点如下:
易于学习和使用:Shiro 设计简单,易于学习和使用。
灵活性高:Shiro 可以适用于任何应用场景,支持多种应用程序类型。
安全性高:Shiro 的安全性能比较优越,提供了多种方式来保护应用程序的安全性。
扩展性好:Shiro 框架提供了可扩展性插件机制,可以方便地扩展框架的功能,满足不同的需求。
企业级支持:Shiro 是从 Apache 基金会获得支持的框架,拥有众多企业用户。
在对 Shiro 框架有了基本认识之后,我们以智慧小区系统作为实际案例,实现了一个通过 Shiro 框架加固安全性的手机号验证码登录功能,为小区会员提供了便捷、安全的登录方法。
技术栈
Spring Boot、Nacos、Redis、Shiro、MQ、阿里云短信发送服务
smart-community-member
配置文件
在Resource包下编写bootsrap.yml文件,通过nacos远程拉取配置中心的配置文件
bootstrap.yml
spring:
application:
name: smart-community-member
profiles:
active: @project.active@
cloud:
nacos:
config:
server-addr: @NACOS-HOST@:@NACOS-PORT@
namespace: @NACOS-NS@
file-extension: yml
shared-configs:
- data-id: common-db.yml
stream:
rocketmq:
binder:
name-server: 你的服务端口
producer-out-0:
producer:
group: member-sms-group
bindings:
producer-out-0:
destination: member-sms-topic
redis:
host: 127.0.0.1
timeout: 5s
connect-timeout: 3s
port: 6379
password: ""
database: 1
jedis:
pool:
max-wait: 5s
seata:
tx-service-group: member_register_tx_group
service:
vgroup-mapping:
member_register_tx_group: default
config:
type: nacos
nacos:
server-addr: @NACOS-HOST@:@NACOS-PORT@
group: DEFAULT_GROUP
data-id: seataServer.properties
registry:
type: nacos
nacos:
server-addr: @NACOS-HOST@:@NACOS-PORT@
group: DEFAULT_GROUP
nacos远程配置中心配置
server:
port: 8090
spring:
cloud:
nacos:
discovery:
server-addr: 你自己的端口号
datasource:
druid:
url: jdbc:mysql://localhost:3306/smart-member
username: "root"
password: "123"
max-active: 8
micro:
servers:
integral:
name: "smart-community-integral"
path: "/integral"
控制层
/**
* 通过手机号发送短信验证码
*/
@GetMapping("/verify/code")
@ApiOperation("通过手机号发送短信验证码")
@ApiImplicitParam(name = "phone",value = "手机号")
public ResponseEntity<String> verify(String phone){
return ResponseEntity.success(memberService.getverified(phone));
}
/**
* 登录校验
*/
@PostMapping("/login")
@ApiOperation("登录校验")
@ApiImplicitParams({@ApiImplicitParam(name = "phone",value = "手机号"),
@ApiImplicitParam(name = "code",value = "验证码")})
public ResponseEntity<String> login(String phone,String code){
return ResponseEntity.success(memberService.login(phone,code));
}
业务实现层
/**
* 生成验证码
* 保存到redis中 设置过期时间 60s
*
* @param phone
* @return
*/
@Override
public String getverified(String phone) {
//1.去redis数据库中查询手机号是否存在验证码
Object phoneNumber = redisDao.get(CODE_SUFFIX_KEY + phone);
if (ObjectUtils.isEmpty(phoneNumber)){
//不存在就生成验证码
String code = RandomUtil.randomNumbers(4);
redisDao.set(CODE_SUFFIX_KEY+phone,code,Duration.ofMinutes(1));
//通过MQ异步消息发送验证码 stream流实现统一的消息发送验证管理
streamBridge.send("producer-out-0", MessageBuilder.withPayload(PhoneMessageDto.builder().phone(phone).code(code).build()).build());
}
return "success";
}
/**
* 验证码登录
* @param phone
* @param code
* @return
*/
@Override
public String login(String phone, String code) {
SecurityUtils.getSubject().login(new UsernamePasswordToken(phone,code));
return SecurityUtils.getSubject().getSession().getId().toString();
}
JavaConfig
@Configuration
public class ShiroConfiguration {
@Bean
public PhoneAndCodeRealm realm(){
return new PhoneAndCodeRealm();
}
@Bean
public DefaultWebSecurityManager securityManager(PhoneAndCodeRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
definition.addPathDefinition("/member/verify/code", "anon");
definition.addPathDefinition("/member/login", "anon");
// 其他路径都需要认证才能访问
definition.addPathDefinition("/**", "authc");
return definition;
}
}
自定义Realm
public class PhoneAndCodeRealm extends AuthenticatingRealm {
@Resource
private MemberService memberService;
@Resource
private RedisDao redisDao;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String phone = (String) token.getPrincipal();
//根据手机号key获取验证码
Object code = redisDao.get("code:verify:phone:"+phone);
//判断验证码是否正确
if (ObjectUtils.isEmpty(code)){
throw new MemberException(ResponseCode.CODE_ERROR);
}
return new SimpleAuthenticationInfo(phone,code.toString(),null,phone);
}
}
smart-community-sms-aliyun
配置文件
sms:
aliyun:
access-key-id: "你自己的id"
access-key-secret: "你自己的secret"
endpoint: "dysmsapi.aliyuncs.com"
sign-name: "智慧小区"
template-code: "自己的code"
template-name: code
spring:
cloud:
stream:
function:
definition: consumer
rocketmq:
binder:
name-server: 你自己的服务
bindings:
consumer-in-0:
consumer:
messageModel: BROADCASTING
bindings:
consumer-in-0:
destination: member-sms-message
group: broadcast-consumer
server:
port: 8887
javaConfig
@Configuration
@EnableConfigurationProperties(AliyunSmsProperties.class)
public class AliyunSmsConfig {
@Autowired
private AliyunSmsProperties smsProperties;
@Bean
public Client client() throws Exception {
Config config = new Config();
config.setAccessKeyId(smsProperties.getAccessKeyId());
config.setAccessKeySecret(smsProperties.getAccessKeySecret());
config.setEndpoint(smsProperties.getEndpoint());
return new Client(config);
}
}
@Data
@ConfigurationProperties("sms.aliyun")
public class AliyunSmsProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String signName;
private String templateCode;
private String templateName;
}
DTO层
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PhoneMessageDTO {
/**
* 手机号
*/
private String phone;
/**
* 验证码
*/
private String code;
}
listener层
@Service
public class SmsListener {
@Resource
private SmsService smsService;
@Bean
public Consumer<Message<PhoneMessageDTO>> consumer(){
return phoneMsg -> {
smsService.sendMsg(phoneMsg.getPayload().getPhone(),phoneMsg.getPayload().getCode());
};
}
}
service实现层
@Service
public class SmsServiceimpl implements SmsService {
@Resource
private Client client;
@Resource
private AliyunSmsProperties smsProperties;
@Resource
private ObjectMapper objectMapper;
@Override
public void sendMsg(String phone, String code) {
try {
// 1.创建hashMap集合
HashMap map = new HashMap();
// 2.将验证码放入map集合中
map.put("code",code);
// 3.创建发送短信的请求对象
SendSmsRequest smsRequest = new SendSmsRequest();
// 4.设置手机号码
smsRequest.setPhoneNumbers(phone);
// 5.设置短信模板
smsRequest.setTemplateCode(smsProperties.getTemplateCode());
// 6.设置签名
smsRequest.setSignName(smsProperties.getSignName());
// 7.设置模板参数
smsRequest.setTemplateParam(objectMapper.writeValueAsString(map));
// 8.发送短信
client.sendSms(smsRequest);
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试
验证码的发送
在redis中查看验证码是否生成
使用手机号验证码登录