概述
4A(認證Authentication、授權Authorization、賬號Account、審計Audit)是現代任何IT系統中很基礎但非常重要的部分,無論是傳統管理信息系統還是互聯網項目,出於保護業務數據和應用自身的安全,都會設計自己的登錄和資源授權策略。最近項目中需要登錄和權限相關的功能,項目為spring-boot工程,現在流行的權限驗證框架有shiro和spring-security,shiro相對spring-security來說學習難度要低一點,也是比較成熟的產品,因此選擇shiro作為項目的權限驗證框架。
步驟
添加依賴
spring boot的版本為2.1.7.RELEASE。如果大量依賴spring的項目,可以用https://start.spring.io/
patchca是驗證碼部分
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
</parent>
shiro-spring是用的最新的版本。patchca是用於驗證碼。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>com.github.bingoohuang</groupId>
<artifactId>patchca</artifactId>
<version>0.0.1</version>
</dependency>
</dependencies>
配置SecurityManager
在spring boot項目中去掉了復雜的各種xml配置,改為在Java文件中配置各種bean
@Bean(name = "securityManager")
public org.apache.shiro.mgt.SecurityManager defaultWebSecurityManager(@Autowired UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 關聯realm
securityManager.setRealm(userRealm);
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
配置ShiroFilterFactoryBean
可以添加Filter,以及各種資源的權限類型anon、authc、user、perms、role。ShiroFilterFactoryBean(該類實現了FactoryBean接口,在IOC容器的基礎上給Bean的實現加上了一個簡單工廠模式和裝飾模式 我們可以在getObject()方法中靈活配置和擴展)
/**
* 創建ShiroFilterFactoryBean shiro過濾bean
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Autowired org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
/**
* anon: 無需認證(登錄)可以訪問
* authc: 必須認證才可以訪問
* user: 如果使用rememberMe功能可以直接訪問
* perms: 該資源必須得到資源權限才可以訪問
* role: 該資源必須得到角色權限才可以訪問
*/
Map<String, String> filerMap = new LinkedHashMap<>(); // 順序的map
filerMap.put("/login", "anon");
filerMap.put("/validCode", "anon");
filerMap.put("/**", "authc");
shiroFilterFactoryBean.setLoginUrl("/user/login.html");
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
shiroFilterFactoryBean.setSuccessUrl("/index");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filerMap);
return shiroFilterFactoryBean;
}
創建和配置Realm
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 執行授權邏輯
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行授權邏輯1");
//給資源進行授權,這里暫時寫死,實際需要從數據庫中獲取當前用戶的資源權限
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("user:add");
info.addStringPermission("user:update");
return info;
}
/**
* 執行認證邏輯
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
ValidCodeUserPassWordToken token = (ValidCodeUserPassWordToken)authenticationToken;
String validCode = token.getValidCode();
if(StringUtils.isEmpty(validCode)) {
throw new AuthenticationException("未輸入驗證碼");
}
//校驗碼部分
Subject subject = SecurityUtils.getSubject();
ValidationCode oldValidCode = (ValidationCode); subject.getSession().getAttribute("VALIDCODE");
subject.getSession().removeAttribute("VALIDCODE");
if(oldValidCode.isExpired()) {
throw new AuthenticationException("驗證碼已過期");
}
if(!oldValidCode.valid(validCode)) {
throw new AuthenticationException("驗證碼輸入錯誤");
}
//實際需要根據賬號,查詢當前用戶信息
User user = new User();
user.setId(123);
user.setName("xs");
user.setPassword("123");
ByteSource salt= ByteSource.Util.bytes(user.getId().toString());
Object password = new SimpleHash("MD5", user.getPassword(), salt, 2);
return new SimpleAuthenticationInfo(
user,
"297254e9bfe0b8f39c682eda30bb9be0", //密碼
salt,
getName()
);
}
}
配置UserRealm為Bean
@Bean
public UserRealm userRealm() {
UserRealm myShiroRealm = new UserRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
myShiroRealm.setCachingEnabled(true);
//啟用身份驗證緩存,即緩存AuthenticationInfo信息,默認false
myShiroRealm.setAuthenticationCachingEnabled(false);
//緩存AuthenticationInfo信息的緩存名稱
myShiroRealm.setAuthenticationCacheName("authenticationCache");
//啟用授權緩存,即緩存AuthorizationInfo信息,默認false
myShiroRealm.setAuthorizationCachingEnabled(true);
//緩存AuthorizationInfo信息的緩存名稱
myShiroRealm.setAuthorizationCacheName("authorizationCache");
return myShiroRealm;
}
/**
* 密碼加密
*/
private HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:這里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於
return hashedCredentialsMatcher;
}
權限注解
@RequestMapping("/add")
@RequiresPermissions("user:add")
public String add() {
return "user/add";
}
校驗碼
/**
* 校驗碼
* @author Administrator
*
*/
public class ValidationCode {
public final static String VALID_CODE_NAME = "VALIDCODE";
private String code;
private Date createTime;
private int expireMillisecond = 6000;
private ValidationCode(String code) {
this.code = code;
this.createTime = new Date();
}
public static ValidationCode create(String code) {
return new ValidationCode(code);
}
/**
* 是否過期
*/
public boolean isExpired() {
Long between_Millisecond = new Date().getTime()-createTime.getTime();
return between_Millisecond.intValue() > expireMillisecond;
}
/**
* 與客戶端code比較是否一致
*/
public boolean valid(String newCode) {
return this.code.equalsIgnoreCase(newCode);
}
}
檢驗碼生成類
@Controller
@RequestMapping
public class ValidationCodeController {
@RequestMapping("/validCode")
public void captcha(HttpServletRequest request, HttpServletResponse response,
@RequestParam(name = "w", defaultValue = "90") Integer width,
@RequestParam(name = "h", defaultValue = "38") Integer height,
@RequestParam(name = "n", defaultValue = "4") Integer number) throws IOException {
ConfigurableCaptchaService configurableCaptchaService = new ConfigurableCaptchaService();
configurableCaptchaService.setColorFactory(new SingleColorFactory(new Color(25, 60, 170)));
configurableCaptchaService
.setFilterFactory(new CurvesRippleFilterFactory(configurableCaptchaService.getColorFactory()));
RandomFontFactory randomFontFactory = new RandomFontFactory();
randomFontFactory.setMinSize(30);
randomFontFactory.setMaxSize(30);
RandomWordFactory randomWordFactory = new RandomWordFactory();
randomWordFactory.setMinLength(number);
randomWordFactory.setMaxLength(number);
configurableCaptchaService.setWordFactory(randomWordFactory);
configurableCaptchaService.setFontFactory(randomFontFactory);
configurableCaptchaService.setHeight(height);
configurableCaptchaService.setWidth(width);
response.setContentType("image/png");
response.setHeader("Cache-Control", "no-cache, no-store");
response.setHeader("Pragma", "no-cache");
long time = System.currentTimeMillis();
response.setDateHeader("Last-Modified", time);
response.setDateHeader("Date", time);
response.setDateHeader("Expires", time);
// 將VALIDCODE放入Session中
ServletOutputStream stream = null;
try {
HttpSession session = request.getSession();
stream = response.getOutputStream();
String validate_code = EncoderHelper.getChallangeAndWriteImage(configurableCaptchaService,
"png", stream);
session.setAttribute(ValidationCode.VALID_CODE_NAME, ValidationCode.create(validate_code));
stream.flush();
} finally {
if (stream != null) {
stream.close();
}
}
}
}
統一異常處理
/**
* 統一異常處理
*/
@RestController
@ControllerAdvice
public class ControllerExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(DataAccessException.class)
public Object handleDuplicateKeyException(DataAccessException e){
logger.error(e.getMessage(), e);
return ResultUtil.error("數據庫中已存在該記錄");
}
@ExceptionHandler(AuthorizationException.class)
public Object handleAuthorizationException(AuthorizationException e){
logger.error(e.getMessage(), e);
return ResultUtil.error("沒有權限,請聯系管理員授權");
}
@ExceptionHandler(Exception.class)
public Object handleException(Exception e){
logger.error(e.getMessage(), e);
return ResultUtil.error(e.getMessage());
}
@ExceptionHandler(IncorrectCredentialsException.class)
public Object handleException(IncorrectCredentialsException e){
logger.error(e.getMessage(), e);
return ResultUtil.error("用戶名或者密碼不對");
}
@ExceptionHandler(UnknownAccountException.class)
public Object handleException(UnknownAccountException e){
logger.error(e.getMessage(), e);
return ResultUtil.error("請輸入正確的賬戶");
}
}
總結
目前搭建的項目,還沒有從數據庫獲取數據,登錄和權限獲取的數據目前都是寫死的。但是基本架子已經搭建好了,只需要在UserRealm中注入UserService類,提供數據庫獲取數據的服務即可。還有基於注解權限的方式需要注入LifecycleBeanPostProcessor和DefaultAdvisorAutoProxyCreator,並且DefaultAdvisorAutoProxyCreator.setProxyTargetClass(true)
。