shiro和spring security 都是安全框架,都可以授權認證。
對比來講spring security自定義能力更強點,shiro單純配置來說更復雜點.
不過我技術不精,還是使用我更熟悉的shiro來進行授權認證功能的實現
下圖是shiro的3層構造,而我們寫也是根據這三層構造來寫的,用戶->安全事務管理器->realm對象
1.依賴shiro包
還是在pom文件寫入
<!--shiro整合spring-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.1</version>
</dependency>
2.創建config文件和Realm文件
config文件:
public class ShiroConfig {
//shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
//關聯SecurityManager
bean.setSecurityManager(securityManager);
return bean;
}
//DefaultwebSecurityManager 配置核心安全事務管理器
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("myRealm") MyRealm myRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//關聯Realm
securityManager.setRealm(myRealm);
return securityManager;
}
//創建realm對象(需要自定義)
@Bean
public MyRealm myRealm(@Qualifier("credentialsMatcher") HashedCredentialsMatcher credentialsMatcher){
MyRealm myRealm=new MyRealm();
myRealm.setCredentialsMatcher(credentialsMatcher);
return myRealm;
}
//以上三層是shiro的三層結構
//下面是附加的
// 配置密碼比較器(密碼加密)
@Bean(name="credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
//RetryLimitHashedCredentialsMatcher爲另外類的構造函數
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");//散列算法:這裏使用MD5算法;
credentialsMatcher.setHashIterations(0);//散列的次數,比如散列兩次,相當於 md5(md5(""));
return credentialsMatcher;
}
}
Realm文件:
public class MyRealm extends AuthorizingRealm {
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行授權!!!!!");
return null;
}
//認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("執行認證!!!!!");
return null;
}
}
3.完善config文件
config文件:
我們還可以在shiroFilterFactoryBean那一層加配置設置很多東西
//shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
//關聯SecurityManager 配置核心安全事務管理器
bean.setSecurityManager(securityManager);
//配置登錄的URL(未登錄的用戶訪問的頁面)
//bean.setLoginUrl("/auth/login");
// 配置登錄成功的url(登錄後用戶訪問的頁面)
//bean.setSuccessUrl("/auth/index");
//自定義攔截器
Map<String,Filter> MyfiltersMap=new LinkedHashMap<String,Filter>();
//限制同一個賬號的同時在線個數
//MyfiltersMap.put("kickout",kickout)
//配置訪問權限
//<url,權限類型>
Map<String,String> filterChainDefinitionMap=new LinkedHashMap<>();
//注意過濾器配置順序 不能顛倒
//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了,登出後跳轉配置的loginUrl
filterChainDefinitionMap.put("/auth/logout","logout");
// 配置不會被攔截的鏈接 順序判斷
//authc:所有url都必須認證通過纔可以訪問;
// anon:所有url都都可以匿名訪問
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/img/**","anon");
filterChainDefinitionMap.put("/auth/login","anon");
filterChainDefinitionMap.put("/*", "authc");
filterChainDefinitionMap.put("/**", "authc");//表示需要認證纔可以訪問
//設置授權形式的訪問
filterChainDefinitionMap.put("/sardine/**","perms[admin:play]");
//設置未授權界面
bean.setUnauthorizedUrl("/403");
//配置shiro的主要攔截器(默認有個攔截器)
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
anon: 無需認證即可訪問
authc: 需要認證纔可訪問
user: 點擊“記住我”功能可訪問
perms: 擁有權限纔可以訪問
role: 擁有某個角色權限才能訪問
4.完善Realm文件之登錄驗證
前一章偷懶的,這裏補上,創建業務層,並且多加個login方法進行登錄驗證
public interface UsersService {
List<Users> queryUserList();
Users queryUserByUserName(String username);
Users queryUserById(int id);
ResponseBean login(String username,String password);
}
@Service
public class UsersServiceImpl implements UsersService{
@Autowired
private UsersMapper usersMapper;
@Override
public List<Users> queryUserList() {
return usersMapper.queryUserList();
}
@Override
public Users queryUserByUserName(String username) {
return usersMapper.queryUserByUserName(username);
}
@Override
public Users queryUserById(int id) {
return usersMapper.queryUserById(id);
}
@Override
public ResponseBean login(String username, String password) {
//獲取當前用戶
Subject subject= SecurityUtils.getSubject();
//設置當前用戶的登錄數據
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//登錄認證 注意這裏的帳號密碼是跟Realm文件那邊設置的進行對比驗證
try{
subject.login(token);
} catch (IncorrectCredentialsException ice) {
// 捕獲密碼錯誤異常
return new ResponseBean(ResponseCode.Fail.GetValue(),"密碼錯誤",null);
} catch (UnknownAccountException uae) {
// 捕獲未知用戶名異常
return new ResponseBean(ResponseCode.Fail.GetValue(),"賬號異常",null);
} catch (ExcessiveAttemptsException eae) {
// 捕獲錯誤登錄過多的異常
return new ResponseBean(ResponseCode.Fail.GetValue(),"錯誤次數過多,賬號被鎖定,請於10分鐘後再進行嘗試",null);
}catch (LockedAccountException lae){
return new ResponseBean(ResponseCode.Fail.GetValue(),"帳號未激活",null);
}catch (AuthenticationException ae){
return new ResponseBean(ResponseCode.Fail.GetValue(),"帳號不存在",null);
}
ResponseBean responseBean = null;
Users user = queryUserByUserName(username);
subject.getSession().setAttribute("user", user);
SecurityUtils.getSubject().getSession().setTimeout(-1000l);
return new ResponseBean(ResponseCode.Success.GetValue(),"登陸成功",user);
}
}
然後在Realm文件中寫方法
public class MyRealm extends AuthorizingRealm {
@Autowired
private UsersService usersService;
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行授權!!!!!");
return null;
}
//認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("執行認證!!!!!");
//這裏就獲取登錄時當前用戶設置的登錄信息
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//根據當前用戶的用戶名查找數據庫的密碼
Users users=usersService.queryUserByUserName(token.getUsername());
if(users!=null){
//根據用戶名在數據庫找不到,重返login頁面
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("redirect:/login");
modelAndView.addObject("msg","賬號不存在");
//return null;
throw new AuthenticationException("帳號不存在");
}
else{
//根據用戶名在數據庫找到用戶了,那麼獲取密碼,並返回,進行比較
return new SimpleAuthenticationInfo(users.getUsername(),users.getPassword(),this.getClass().getName());
}
}
}
至此,shiro的登錄認證功能基本已配置完。
Realm文件登錄驗證測試(導入thymeleaf):
到實際測試階段,這時,我選擇偷懶,所以先導入themleaf依賴,還是pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
然後繼續yml文件配置
spring下面加入
#thymeleaf設置
#模板編碼
thymeleaf:
encoding: UTF-8
cache: false #啓用模板緩存(開發時建議關閉)
mode: HTML5 #應用於模板的模板模式ervlet
servlet:
content-type: text/html #Content-Type值
enabled: true #啓用MVC Thymeleaf視圖分辨率
prefix: classpath:/templates/ #在構建URL時預先查看名稱的前綴
suffix: .html #構建URL時附加查看名稱的後綴
整體:
spring:
#熱部署
devtools:
restart:
enabled: true
additional-paths: src/main/java
#數據庫配置
datasource:
#1.JDBC
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xx.xxx.xxx.xxx/sardines?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
username: root
password: root
druid:
#2.連接池配置
#初始化連接池的連接數量 大小,最小,最大
initial-size: 5
min-idle: 5
max-active: 20
#配置獲取連接等待超時的時間
max-wait: 60000
#配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
min-evictable-idle-time-millis: 30000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: true
test-on-return: false
# 是否緩存preparedStatement,也就是PSCache 官方建議MySQL下建議關閉 個人建議如果想用SQL防火牆 建議打開
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用於防火牆
filter:
stat:
merge-sql: true
slow-sql-millis: 5000
#3.基礎監控配置
web-stat-filter:
enabled: true
url-pattern: /*
#設置不統計哪些URL
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
session-stat-enable: true
session-stat-max-count: 100
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: true
#設置監控頁面的登錄名和密碼
login-username: admin
login-password: 123456
allow: 127.0.0.1
#deny: 122.1.1.1
#thymeleaf設置
#模板編碼
thymeleaf:
encoding: UTF-8
cache: false #啓用模板緩存(開發時建議關閉)
mode: HTML5 #應用於模板的模板模式ervlet
servlet:
content-type: text/html #Content-Type值
enabled: true #啓用MVC Thymeleaf視圖分辨率
prefix: classpath:/templates/ #在構建URL時預先查看名稱的前綴
suffix: .html #構建URL時附加查看名稱的後綴
#mybatis配置
mybatis:
#設置基本包 # 注意:對應實體類的路徑
type-aliases-package: com.sardine.myproject.pojo
#告訴去哪找xml文件 #注意:一定要對應mapper映射xml文件的所在路徑
mapper-locations: classpath:mybatis/mapper/*.xml
server:
port: 8080
至此,shiro框架的用戶認證和thymeleaf配置就完成了。
實際使用
接下來,就是在前端頁面引用thymeleaf,然後後端寫代碼進行操作了,我暫時做了個簡單的登錄,下面是具體代碼。
ResponseCode文件和UserController文件:
public enum ResponseCode {
Success(200),
Fail(400),
Error(404);
private int value = 0;
private ResponseCode(int value) { //必須是private的,否則編譯錯誤
this.value = value;
}
public int GetValue(){
return this.value;
}
}
@Controller
public class UserController {
@Autowired
private UsersMapper usersMapper;
@Autowired
private UsersService usersService;
@GetMapping({"/"})
public String toLogin(){
return "login";
}
@PostMapping("/login")
public String login(String username,String password){
System.out.println("username:"+username+" password:"+password);
ResponseBean responseBean= usersService.login(username,password);
if(responseBean.getCode()== ResponseCode.Success.GetValue()){
return "index";
}
else{
System.out.println("登錄失敗:"+responseBean.getMsg());
//System.out.println("登錄失敗");
return "login";
}
}
}
UserMapper.xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sardine.mapper.UsersMapper">
<resultMap id="userResultMap" type="com.sardine.pojo.Users">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="username" jdbcType="VARCHAR" property="username"/>
<result column="password" jdbcType="VARCHAR" property="password"/>
<result column="status" jdbcType="INTEGER" property="status"/>
</resultMap>
<sql id="BASE_TABLE">
users a
</sql>
<sql id="BASE_COLUMN">
a.*
</sql>
<select id="queryUserList" resultMap="userResultMap">
SELECT
<include refid="BASE_COLUMN"/>
FROM
<include refid="BASE_TABLE"/>
</select>
<select id="queryUserByUserName" resultMap="userResultMap" parameterType="String">
SELECT
<include refid="BASE_COLUMN"/>
FROM
<include refid="BASE_TABLE"/>
<where>
a.username = #{username}
</where>
</select>
<select id="queryUserById" resultMap="userResultMap" parameterType="int">
SELECT
<include refid="BASE_COLUMN"/>
FROM
<include refid="BASE_TABLE"/>
<where>
id = #{id}
</where>
</select>
</mapper>
前端文件:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>測試</title>
</head>
<body>
<h1>登錄</h1>
<form method="post" th:action="@{/login}">
<p>用戶名: <input type="text" name="username"></p>
<p>密碼: <input type="text" name="password"></p>
<p><input type="submit"></p>
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登錄成功</h1>
</body>
</html>
PS:這裏講幾個點:
1.themleaf要用<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">這句話必不可少
2.themleaf語法請自學
3.爲了統一規格我額外建了個enum類,嫌麻煩直接寫數字也可以
4.ReaponseBean是比較爛大街的自己寫的,返回code、msg、data這三個數據的一個類,這裏就不貼出來了。
5.由於我寫的是加密的密碼比較器,所以數據庫那邊的密碼不是123456,而是md5加密後的數據
密碼:123456 轉換成這裏寫的md5加密後的結果是:e10adc3949ba59abbe56e057f20f883e
5.用戶認證做完,繼續完善Realm文件的權限認證
修改Realm文件,這裏也就不寫實際應用了,無非就是能進去,不能進去,然後在config文件已經設置過授權頁面和未授權頁面的跳轉了。
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行授權!!!!!");
SimpleAuthorizationInfo Info = new SimpleAuthorizationInfo();
//獲取登錄後的用戶,如果用戶有admin角色或用戶的Principal信息保存的是sardine用戶名,那麼添加權限
Subject subject= SecurityUtils.getSubject();
if(subject.hasRole("admin")||subject.getPrincipal().equals("sardine")) {
Info.addStringPermission("admin:play");
}
return Info;
}
PS:這裏的addStringPermission裏面的參數,可以改成通過自動裝配註解,從數據庫取權限名稱。