Java秒殺系統(tǒng)(五):整合Shiro實(shí)現(xiàn)用戶登錄認(rèn)證

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。



摘要:本篇博文是“Java秒殺系統(tǒng)實(shí)戰(zhàn)系列文章”的第五篇,在本篇博文中,我們將整合權(quán)限認(rèn)證-授權(quán)框架Shiro,實(shí)現(xiàn)用戶的登陸認(rèn)證功能,主要用于:要求用戶在搶購(gòu)商品或者秒殺商品時(shí),限制用戶進(jìn)行登陸!并對(duì)于特定的url(比如搶購(gòu)請(qǐng)求對(duì)應(yīng)的url)進(jìn)行過(guò)濾(即當(dāng)用戶訪問(wèn)指定的url時(shí),需要要求用戶進(jìn)行登陸)。

內(nèi)容:對(duì)于Shiro,相信各位小伙伴應(yīng)該聽(tīng)說(shuō)過(guò),甚至應(yīng)該也使用過(guò)!簡(jiǎn)單而言,它是一個(gè)很好用的用戶身份認(rèn)證、權(quán)限授權(quán)框架,可以實(shí)現(xiàn)用戶登錄認(rèn)證,權(quán)限、資源授權(quán)、會(huì)話管理等功能,在本秒殺系統(tǒng)中,我們將主要采用該框架實(shí)現(xiàn)對(duì)用戶身份的認(rèn)證和用戶的登錄功能。

值得一提的是,本篇博文介紹的“Shiro實(shí)現(xiàn)用戶登錄認(rèn)證”功能模塊涉及到的數(shù)據(jù)庫(kù)表為用戶信息表user,下面進(jìn)入代碼實(shí)戰(zhàn)環(huán)節(jié)。

<!--shiro權(quán)限控制-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>

(2) 緊接著是在UserController控制器中開(kāi)發(fā)用戶前往登錄、用戶登錄以及用戶退出登錄的請(qǐng)求對(duì)應(yīng)的功能方法,其完整的源代碼如下所示:  

@Autowired
private Environment env;

//跳到登錄頁(yè)
@RequestMapping(value = {"/to/login","/unauth"})
public String toLogin(){
return "login";
}

//登錄認(rèn)證
@RequestMapping(value = "/login",method = RequestMethod.POST)
public String login(@RequestParam String userName, @RequestParam String password, ModelMap modelMap){
String errorMsg="";
try {
if (!SecurityUtils.getSubject().isAuthenticated()){
String newPsd=new Md5Hash(password,env.getProperty("shiro.encrypt.password.salt")).toString();
UsernamePasswordToken token=new UsernamePasswordToken(userName,newPsd);
SecurityUtils.getSubject().login(token);
}
}catch (UnknownAccountException e){
errorMsg=e.getMessage();
modelMap.addAttribute("userName",userName);
}catch (DisabledAccountException e){
errorMsg=e.getMessage();
modelMap.addAttribute("userName",userName);
}catch (IncorrectCredentialsException e){
errorMsg=e.getMessage();
modelMap.addAttribute("userName",userName);
}catch (Exception e){
errorMsg="用戶登錄異常,請(qǐng)聯(lián)系管理員!";
e.printStackTrace();
}
if (StringUtils.isBlank(errorMsg)){
return "redirect:/index";
}else{
modelMap.addAttribute("errorMsg",errorMsg);
return "login";
}
}

//退出登錄
@RequestMapping(value = "/logout")
public String logout(){
SecurityUtils.getSubject().logout();
return "login";
}

其中,在匹配用戶的密碼時(shí),我們?cè)谶@里采用的Md5Hash的方法,即MD5加密的方式進(jìn)行匹配(因?yàn)閿?shù)據(jù)庫(kù)的user表中用戶的密碼字段存儲(chǔ)的正是采用MD5加密后的加密串)

前端頁(yè)面login.jsp的內(nèi)容比較簡(jiǎn)單,只需要用戶輸入最基本的用戶名和密碼即可,如下圖所示為該頁(yè)面的部分核心源代碼:  



當(dāng)前端提交“用戶登錄”請(qǐng)求時(shí),將以“提交表單”的形式將用戶名、密碼提交到后端UserController控制器對(duì)應(yīng)的登錄方法中,該方法首先會(huì)進(jìn)行最基本的參數(shù)判斷與校驗(yàn),校驗(yàn)通過(guò)之后,會(huì)調(diào)用Shiro內(nèi)置的組件SecurityUtils.getSubject().login()方法執(zhí)行登錄操作,其中的登錄操作將主要在 “自定義的Realm的doGetAuthenticationInfo方法”中執(zhí)行。

(4) 接下來(lái)是基于Shiro的AuthorizingRealm,開(kāi)發(fā)自定義的Realm,并實(shí)現(xiàn)其中的用戶登錄認(rèn)證方法,即doGetAuthenticationInfo()方法。其完整的源代碼如下所示:  

/**
* 用戶自定義的realm-用于shiro的認(rèn)證、授權(quán)
* @Author:debug (SteadyJack)
* @Date: 2019/7/2 17:55
**/
public class CustomRealm extends AuthorizingRealm{
private static final Logger log= LoggerFactory.getLogger(CustomRealm.class);

private static final Long sessionKeyTimeOut=3600_000L;
@Autowired
private UserMapper userMapper;

//授權(quán)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}

//認(rèn)證-登錄
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token= (UsernamePasswordToken) authenticationToken;
String userName=token.getUsername();
String password=String.valueOf(token.getPassword());
log.info("當(dāng)前登錄的用戶名={} 密碼={} ",userName,password);

User user=userMapper.selectByUserName(userName);
if (user==null){
throw new UnknownAccountException("用戶名不存在!");
}
if (!Objects.equals(1,user.getIsActive().intValue())){
throw new DisabledAccountException("當(dāng)前用戶已被禁用!");
}
if (!user.getPassword().equals(password)){
throw new IncorrectCredentialsException("用戶名密碼不匹配!");
}

SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(user.getUserName(),password,getName());
setSession("uid",user.getId());
return info;
}

/**
* 將key與對(duì)應(yīng)的value塞入shiro的session中-最終交給HttpSession進(jìn)行管理(如果是分布式session配置,那么就是交給redis管理)
* @param key
* @param value
*/
private void setSession(String key,Object value){
Session session=SecurityUtils.getSubject().getSession();
if (session!=null){
session.setAttribute(key,value);
session.setTimeout(sessionKeyTimeOut);
}
}
}

其中,userMapper.selectByUserName(userName);主要用于根據(jù)userName查詢用戶實(shí)體信息,其對(duì)應(yīng)的動(dòng)態(tài)Sql的寫(xiě)法如下所示:    

<!--根據(jù)用戶名查詢-->
<select id="selectByUserName" resultType="com.debug.kill.model.entity.User">
SELECT <include refid="Base_Column_List"/>
FROM user
WHERE user_name = #{userName}
</select>

值得一提的是,當(dāng)用戶登錄成功時(shí)(即用戶名和密碼的取值跟數(shù)據(jù)庫(kù)的user表相匹配),我們會(huì)借助Shiro的Session會(huì)話機(jī)制將當(dāng)前用戶的信息存儲(chǔ)至服務(wù)器會(huì)話中,并緩存一定時(shí)間?。ㄔ谶@里是3600s,即1個(gè)小時(shí))!


(4) 最后是我們需要實(shí)現(xiàn)“用戶在訪問(wèn)待秒殺商品詳情或者搶購(gòu)商品或者任何需要進(jìn)行攔截的業(yè)務(wù)請(qǐng)求時(shí),如何自動(dòng)檢測(cè)用戶是否處于登錄的狀態(tài)?如果已經(jīng)登錄,則直接進(jìn)入業(yè)務(wù)請(qǐng)求對(duì)應(yīng)的方法邏輯,否則,需要前往用戶登錄頁(yè)要求用戶進(jìn)行登錄”。


基于這樣的需求,我們需要借助Shiro的組件ShiroFilterFactoryBean 實(shí)現(xiàn)“用戶是否登錄”的判斷,以及借助FilterChainDefinitionMap攔截一些需要授權(quán)的鏈接URL,其完整的源代碼如下所示:  

/**
* shiro的通用化配置
* @Author:debug (SteadyJack)
* @Date: 2019/7/2 17:54
**/
@Configuration
public class ShiroConfig {

@Bean
public CustomRealm customRealm(){
return new CustomRealm();
}

@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
securityManager.setRememberMeManager(null);
return securityManager;
}

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager());
bean.setLoginUrl("/to/login");
bean.setUnauthorizedUrl("/unauth");
//對(duì)于一些授權(quán)的鏈接URL進(jìn)行攔截
Map<String, String> filterChainDefinitionMap=new HashMap<>();
filterChainDefinitionMap.put("/to/login","anon");
filterChainDefinitionMap.put("/**","anon");

filterChainDefinitionMap.put("/kill/execute","authc");
filterChainDefinitionMap.put("/item/detail/*","authc");

bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}

}


從上述該源代碼中可以看出,Shiro的ShiroFilterFactoryBean組件將會(huì)對(duì) URL=/kill/execute 和 URL=/item/detail/*  的鏈接進(jìn)行攔截,即當(dāng)用戶訪問(wèn)這些URL時(shí),系統(tǒng)會(huì)要求當(dāng)前的用戶進(jìn)行登錄(前提是用戶還沒(méi)登錄的情況下!如果已經(jīng)登錄,則直接略過(guò),進(jìn)入實(shí)際的業(yè)務(wù)模塊?。?br>
除此之外,Shiro的ShiroFilterFactoryBean組件還設(shè)定了 “前往登錄頁(yè)”和“用戶沒(méi)授權(quán)/沒(méi)登錄的前提下的調(diào)整頁(yè)”的鏈接,分別是 /to/login 和 /unauth!

(5) 至此,整合Shiro框架實(shí)現(xiàn)用戶的登錄認(rèn)證的前后端代碼實(shí)戰(zhàn)已經(jīng)完畢了,將項(xiàng)目/系統(tǒng)運(yùn)行在外置的tomcat服務(wù)器中,打開(kāi)瀏覽器即可訪問(wèn)進(jìn)入“待秒殺商品的列表頁(yè)”,點(diǎn)擊“詳情”,此時(shí),由于用戶還沒(méi)登陸,故而將跳轉(zhuǎn)至用戶登錄頁(yè),如下圖所示:  



輸入用戶名:debug,密碼:123456,點(diǎn)擊“登錄”按鈕,即可登錄成功,并成功進(jìn)入“詳情頁(yè)”,如下圖所示:   


登錄成功之后,再回到剛剛上一個(gè)列表頁(yè),即“待秒殺商品的列表頁(yè)”,再次點(diǎn)擊“詳情”按鈕,此時(shí)會(huì)直接進(jìn)入“待秒殺商品的詳情頁(yè)”,而不會(huì)跳轉(zhuǎn)至“用戶登錄頁(yè)”,而且用戶的登錄態(tài)將會(huì)持續(xù)1個(gè)小時(shí)?。ㄟ@是借助Shiro的Session的來(lái)實(shí)現(xiàn)的)。  

補(bǔ)充:

1、目前,這一秒殺系統(tǒng)的整體構(gòu)建與代碼實(shí)戰(zhàn)已經(jīng)全部完成了,該秒殺系統(tǒng)對(duì)應(yīng)的視頻教程的鏈接地址為:https://www.fightjava.com/web/index/course/detail/6,可以點(diǎn)擊鏈接進(jìn)行試看以及學(xué)習(xí),實(shí)戰(zhàn)期間有任何問(wèn)題都可以留言或者與Debug聯(lián)系、交流!

2、另外,Debug也開(kāi)源了該秒殺系統(tǒng)對(duì)應(yīng)的完整的源代碼以及數(shù)據(jù)庫(kù),其地址可以來(lái)這里下載:https://gitee.com/steadyjack/SpringBoot-SecondKill 記得Fork跟Star啊?。?!

3、最后,不要忘記了關(guān)注一下Debug的技術(shù)微信公眾號(hào):