一般來說,Web 應用的安全性包括用戶認證(Authentication)和用戶授權(Authorization)兩個部分。
- 用戶認證:指的是驗證某個用戶是否爲系統中的合法主體,也就是說用戶能否訪問該系統。(登錄)
- 用戶授權:指的是該登錄用戶是否有執行某個操作的權限。(用戶與管理員,遊客與商家)
集成SpringSecurity
- 在項目導入Spring Security的依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--引入Thymeleaf依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--Security與Thymeleaf整合-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
注意:
- 角色授權:授權代碼需要加ROLE_(ROLE_ADMIN,ROLE_USER),即數據庫角色表字段對應的值需要有ROLE_前綴,controller上使用時不要加前綴。
- 權限授權:設置和使用時,名稱保持一至即可
- 編寫數據庫查詢該用戶的服務類,以便授權調用
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//以下兩個是分別獲取登陸用戶的角色和權限,可以選擇利用角色作爲授權,或者權限作爲授權
Collection<GrantedAuthority> roleauthorities = new ArrayList<>();
/* Collection<GrantedAuthority> pageauthorities = new ArrayList<>();*/
//角色 數據庫獲取該用戶的角色名
Set<role> roles = roleService.roles(username);
//權限 數據庫獲取該用戶權限名
/*List<user_Page> user_pages = userPageService.user_Page_List(username);*/
//將用戶角色放入roleauthorities
if(roles != null){
for (role role : roles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRolename());
roleauthorities.add(authority);
}
}
//將用戶權限放入pageauthorities
/* if (user_pages !=null && !user_pages.isEmpty()){
for (user_Page userPage:user_pages){
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(userPage.getRoleCode());
pageauthorities.add(authority);
}
}*/
//根據角色授權還是根據權限授權自己選,此處,根據用戶角色授權
UserDetails roleuserDetails = new User(login.getLoginusername(),bCryptPasswordEncoder.encode(login.getLoginpassword()),roleauthorities);
/* UserDetails pageuserDetails = new User(login.getLoginusername(),bCryptPasswordEncoder.encode(login.getLoginpassword()),pageauthorities);*/
return roleuserDetails;
}
}
- 編寫配置類
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//定製請求的授權規則
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/user/**").hasAnyRole("ROLE_USER","ROLE_ADMIN")
.antMatchers("/admin/**").hasRole("ROLE_ADMIN")
/*.antMatchers("/user/**").hasAnyAuthority("user","admin")*/
/*.antMatchers("/admin/**").hasAuthority("admin")*/
/*hasIpAddress("127.0.0.1") 只有發送的Ip匹配時才允許*/
//開啓自動配置的登錄功能:如果沒有權限,就會跳轉到登錄頁面!
http.formLogin()
.usernameParameter("username") //前端傳來的表單name格式
.passwordParameter("password") //前端傳來的表單name格式
.loginPage("/toLogin") //沒有登錄所跳轉到的登錄頁
.loginProcessingUrl("/login"); // 登陸表單提交請求
//開啓自動配置的註銷的功能
// /logout 註銷請求
// .logoutSuccessUrl("/"); 註銷成功來到首頁
http.csrf().disable();//關閉csrf功能:跨站請求僞造,默認只能通過post方式提交logout請求
http.logout().logoutSuccessUrl("/");// 註銷成功來到首頁
//記住我
http.rememberMe().rememberMeParameter("remember");
}
//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//spring security 官方推薦的是使用bcrypt加密方式,基於內存的驗證
/*auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("ROLE_ADMIN","ROLE_USER")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("ROLE_USER");*/
//基於數據庫的認證
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
- 與thymeleaf整合
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<div sec:authorize="!isAuthenticated()>
//沒有登錄,就顯示。
</div>
<div sec:authorize="isAuthenticated()>
//登錄後才顯示
用戶名:<span sec:authentication="principal.username"></span>
角色:<span sec:authentication="principal.authorities"></span>
</div>
<div sec:authorize="hasRole('ROLE_ADMIN')">
//admin角色纔看得見
</div>
<div sec:authorize="hasAnyRole('ROLE_ADMIN','ROLE_USER')">
//admin和user角色纔看得見
</div>
<div sec:authorize="hasAuthority('admin')">
//admin權限的用戶就能看到
</div>
<div sec:authorize="hasAnyAuthority('admin','user')">
//admin和user權限的用戶就能看到
</div>
集成Shiro
- 導入shiro
<!--導入shiro安全框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.1</version>
</dependency>
<!--引入Thymeleaf依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- thymelealf與shiro的整合 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
- 自定義UserRealm類:用於查詢用戶的角色和權限信息並保存到權限管理器
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserServiceimpl userServiceimpl; //數據庫操作的實現類
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到當前登錄的用戶信息
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();
/*for (Role role : currentUser.getRoles()) {
//添加角色
info.addRole(role.getRoleName());
//添加權限
for (Permissions permissions : role.getPermissions()) {
info.addStringPermission(permissions.getPermissionsName());
}
}*/
if(currentUser.getLevel() == 1) { //根據數據庫表對應該用戶存儲的等級進行權限授權
info.addStringPermission("user:ordinary");
}else if(currentUser.getLevel() == 2){
info.addStringPermission("user:admin");
}else if(currentUser.getLevel() == 3){
info.addStringPermission("user:admin");
}
return info;
}
//認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken userToken = (UsernamePasswordToken) token; //獲取控制層傳來的token信息。
User user = userServiceimpl.queryByUnameOrEmail(userToken.getUsername());
if (user == null) {//沒有這個用戶
return null;//拋出UnknownAccountException的異常
}
String passwordMD5 = user.getPwdMD5(); //獲取該用戶數據庫用戶表中儲存的加密後的密碼。
Subject currentSubject = SecurityUtils.getSubject();
Session session = currentSubject.getSession();
session.setAttribute("loginUser",user); //將該用戶信息放入當前會話中
//可以加密, MD5,MD5鹽值加密
//密碼認證shiri自動做,已經加密
ByteSource salt = ByteSource.Util.bytes(user.getUsername()); //MD5鹽值加密
return new SimpleAuthenticationInfo(user, passwordMD5,salt,"");
}
}
- 配置Shiro配置類
把UserRealm和SecurityManager等加入到spring容器,順便配置cookie,session和密碼加密配置。
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
//設置安全管理器
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的內置過濾器
/*
anon:無需認證可以訪問
authc:必須認證纔可以訪問
user:必須擁有 記住我 功能才能用
perms:擁有對某個資源的權限才能訪問
role:擁有某個角色權限才能訪問
logout:退出登錄
*/
Map<String, String> filterMap = new LinkedMap();
//權限操作
filterMap.put("/login", "anon");
filterMap.put("/logout", "authc");
filterMap.put("/", "anon");
filterMap.put("/function/*", "authc");
filterMap.put("/home","authc,user");
filterMap.put("/admin/*","authc,perms[user:admin]");
filterMap.put("/user/*","authc,user");
filterMap.put("/index", "authc,user");
bean.setFilterChainDefinitionMap(filterMap);
//設置登錄的請求
bean.setLoginUrl("/login");
//設置爲授權的請求
bean.setUnauthorizedUrl("/noauth");
return bean;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//關聯UserRealm
securityManager.setRealm(userRealm);
//關聯記住我功能
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
//創建realm對象,自定義對象類
@Bean(name = "userRealm")
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
// 告訴realm,使用credentialsMatcher加密算法類來驗證密文
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
userRealm.setCachingEnabled(false);
return userRealm;
}
/**
* cookie對象
* @return
*/
public SimpleCookie rememberMeCookie() {
// 設置cookie名稱,對應登錄頁面的記住我radio的name
SimpleCookie cookie = new SimpleCookie("rememberMe");
// 設置cookie的過期時間,單位爲秒,這裏爲7天
cookie.setMaxAge(86400*7);
return cookie;
}
/**
* cookie管理對象
* @return
*/
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密鑰
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
/**
* shiro session的管理
*/
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//設置url重新setSessionIdUrlRewritingEnabled值爲false
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
/**
* 加密配置
* @return
*/
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:這裏使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次數,比如散列兩次,相當於 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(1024);
// storedCredentialsHexEncoded默認是true,此時用的是密碼加密用的是Hex編碼;false時用Base64編碼
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
//整合shiroDialect
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
}
- controller層中對登錄的相關操作(部分代碼)
@PostMapping("/toLogin")
public String login(@RequestParam Map<String, Object> formData,
Model model,
RedirectAttributes redirectAttributes) {
//獲取當前的用戶
Subject subject = SecurityUtils.getSubject();
//封裝當前用戶信息成token
UsernamePasswordToken token = new UsernamePasswordToken((String) formData.get("username"), (String) formData.get("password"));
//設置記住我
if (formData.get("rememberMe") == "true") {
token.setRememberMe(true);
}
try {
subject.login(token);//執行登錄的方法。沒有異常就成功!
userMapper.addUserView((String) formData.get("username"));
return "redirect:/index"; //成功登錄~
} catch (UnknownAccountException e) { //用戶名不存在
model.addAttribute("msg", "用戶名或郵箱不存在!");
model.addAttribute("success", false);
return "login";
} catch (IncorrectCredentialsException e) {//密碼錯誤
model.addAttribute("msg", "密碼錯誤!");
model.addAttribute("success", false);
model.addAttribute("username",formData.get("username"));
return "login";
}
}
- Thymeleaf與Shiro的整合操作
<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<shiro:authenticated> //登錄認證後可看
<a href="/1">1111</a>
</shiro>
<shiro:notAuthenticated> //未登錄認證可看,記住我的cookies的自動登錄也算。
<a href="/1">1111</a>
</shiro>
<div shiro:hasPermission="user:admin"> //只有管理員權限能看見
<a href="/1">1111</a>
</div>
<div shiro:hasPermission="user:ordinary"> //用戶權限可見
<a href="/2">2222</a>
</div>
<div shiro:hasRole="user"> //用戶角色可以見
<a href="/2">2222</a>
</div>
shiro標籤的相關說明:
guest標籤
<shiro:guest>
用戶沒有身份驗證時顯示相應信息,即遊客訪問信息。
</shiro:guest>
user標籤
<shiro:user>
用戶已經身份驗證/記住我登錄後顯示相應的信息。
</shiro:user>
authenticated標籤
<shiro:authenticated>
用戶已經身份驗證通過,即Subject.login登錄成功,不是記住我登錄的。
</shiro:authenticated>
notAuthenticated標籤
<shiro:notAuthenticated>
</shiro:notAuthenticated>
用戶已經身份驗證通過,即沒有調用Subject.login進行登錄,包括記住我自動登錄的也屬於未進行身份驗證。
principal標籤
<shiro: principal/>
相當於((User)Subject.getPrincipals()).getUsername()。
<shiro:principal property="username"/>
lacksPermission標籤
<shiro:lacksPermission name="org:create">
如果當前Subject沒有權限將顯示body體內容。
</shiro:lacksPermission>
hasRole標籤
<shiro:hasRole name="admin">
如果當前Subject有角色將顯示body體內容。
</shiro:hasRole>
hasAnyRoles標籤
<shiro:hasAnyRoles name="admin,user">
//如果當前Subject有任意一個角色(或的關係)將顯示body體內容。
</shiro:hasAnyRoles>
lacksRole標籤
<shiro:lacksRole name="abc">
如果當前Subject沒有角色將顯示body體內容。
</shiro:lacksRole>
hasPermission標籤
<shiro:hasPermission name="user:admin">
如果當前Subject有權限將顯示body體內容
</shiro:hasPermission>