概述
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是验证码部分
org.springframework.boot
spring-boot-starter-parent
2.1.7.RELEASE
shiro-spring是用的最新的版本。patchca是用于验证码。
org.springframework.boot
spring-boot-starter-web
org.apache.shiro
shiro-spring
1.4.1
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-starter-data-jpa
com.alibaba
druid
1.1.10
log4j
log4j
1.2.17
com.github.bingoohuang
patchca
0.0.1
配置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 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)
。