Spirngboot模块系列:安全认证Shiro

    科技2026-04-05  6

    1 配置

    pom.xml <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> 配置接口(路由)鉴权 接口可以使用鉴权也可不使用鉴权 序号是否使用鉴权标志1使用authc2不使用anon 使用ShiroFilterFactoryBean配置路由权限自定义权限认证逻辑:继承AuthorizingRealm类 package com.company.ddd.infrastructure.config; import com.company.ddd.infrastructure.util.*; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.beans.factory.annotation.Autowired; import java.util.Map; import java.util.HashMap; import java.util.LinkedHashMap; /** * @author xindaqi * @since 2020-10-08 */ @Configuration public class ShiroConfig { @Bean MyRealm myRealm(){ MyRealm myRealm = new MyRealm(); // 设置鉴权token,前端登录时传递的参数,进行初始化 myRealm.setAuthenticationTokenClass(AuthenticationToken.class); // 自定义密码校验,继承SimpleCredentialsMatcher myRealm.setCredentialsMatcher(new MyCredentialsMatcherUtil()); return myRealm; } @Bean SecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm()); return manager; } @Bean ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); bean.setLoginUrl("/login"); bean.setSuccessUrl("/index"); bean.setUnauthorizedUrl("/unauthorizedurl"); Map<String, String> map = new LinkedHashMap<>(); map.put("/doLogin", "anon"); /** * Swagger2开放,不使用鉴权 */ map.put("/swagger-ui.html", "anon"); map.put("/swagger-resources/**", "anon"); map.put("/v2/**", "anon"); map.put("/webjars/**", "anon"); // map.put("/configuration/security", "anon"); // map.put("/configuration/ui", "anon"); // 开放以api开始的接口,不使用鉴权 map.put("/api/**", "anon"); // 对除了api以外的接口,全部鉴权 map.put("/**", "authc"); bean.setFilterChainDefinitionMap(map); return bean; } }

    2 登录认证

    配置接口鉴权之后,调用接口时需要先登录或者其他鉴权功能,可以自定,这里以登录为例,通过登录service查询数据库用户名和密码,将登陆时的前端传的用户名和密码与数据库比对,若数据库的密码是通过加密的,需要自定义密码校验规则,如使用security生成密码和校验密码。

    package com.company.ddd.infrastructure.util; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; import com.company.ddd.interfaces.dto.user.*; import com.company.ddd.application.service.*; /** * @author xindaqi * @since 2020-10-08 */ public class MyRealm extends AuthorizingRealm { // 数据库查询Service,登录时查询用户和密码 @Autowired private IUserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); LoginInputDTO params = new LoginInputDTO(); params.setUsername(username); // params.setPassword(); LoginOutputDTO loginOutputDTO = userService.login(params); String usernameInDb = loginOutputDTO.getUsername(); String passwordInDb = loginOutputDTO.getPassword(); if(!usernameInDb.equals(username)) { throw new UnknownAccountException("账号不存在"); } return new SimpleAuthenticationInfo(username, passwordInDb, getName()); } }

    3 自定义密码认证

    由于shiro没有提供加密的密码校验,所以数据库存储了加密的密码,就要通过继承SimpleCredentialsMatcher,实现密码校验。

    package com.company.ddd.infrastructure.util; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @author xindaqi * @since 2020-10-11 */ public class MyCredentialsMatcherUtil extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { // Password from user login. String originalPassword = String.valueOf((char[]) token.getCredentials()); // Password in database String sqlOriginalPassword=(String)info.getCredentials(); // 通过security的BCryptPasswordEncoder进行密码校对 BCryptPasswordEncoder pwdCmp = new BCryptPasswordEncoder(); Boolean cmpRes = pwdCmp.matches(originalPassword,sqlOriginalPassword); return cmpRes; } }

    4 登录

    package com.company.ddd.interfaces.facade; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.util.StringUtils; import org.apache.shiro.subject.Subject; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authz.AuthorizationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.company.ddd.domain.common.vo.*; import com.company.ddd.interfaces.dto.user.*; import com.company.ddd.application.service.*; /** * @author xindaqi * @since 2020-10-08 */ @CrossOrigin(origins = "*", maxAge = 3600) @RestController public class Loginout { static Logger logger = LoggerFactory.getLogger(Loginout.class); @RequestMapping(value = "/doLogin", method = RequestMethod.POST) public ResponseVO login(@RequestBody LoginInputDTO params){ Subject subject = SecurityUtils.getSubject(); if (StringUtils.isEmpty(params.getUsername()) || StringUtils.isEmpty(params.getPassword())){ return ResponseVO.empty(); } UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken( params.getUsername(), params.getPassword() ); try { subject.login(usernamePasswordToken); }catch (UnknownAccountException e){ logger.error("用户名不存在!"); return ResponseVO.empty(); }catch (AuthenticationException e){ logger.error("账号或密码错误!"); return ResponseVO.empty(); }catch (AuthorizationException e) { logger.error("没有权限!"); return ResponseVO.empty(); } return ResponseVO.ok(); } @RequestMapping(value = "/login", method = RequestMethod.GET) public String loginStr(){ return "请登录"; } @RequestMapping(value = "/test", method = RequestMethod.GET) public String test(){ return "未授权"; } }

    图4.1 登录

    5 密码加密:security

    5.1 security配置

    package com.company.ddd.infrastructure.config; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; /** * @author xindaqi * @since 2020-10-11 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public BCryptPasswordEncoder passwordEncoderBCrypt() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/**")// 开放所有接口 .permitAll() .and() .csrf() .disable(); } }

    5.2 注册及登录测试

    package com.company.ddd.interfaces.facade; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCrypt; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiOperation; import com.company.ddd.domain.common.vo.*; import com.company.ddd.interfaces.dto.user.*; import com.company.ddd.application.service.*; /** * @author xindaqi * @since 2020-10-06 */ @CrossOrigin(origins = "*", maxAge=3600) @RestController @RequestMapping("/api/user") @Api(tags = "人员配置") public class User{ static Logger logger = LoggerFactory.getLogger(User.class); @Autowired private BCryptPasswordEncoder passwordEncoderBCrypt; @Autowired private IUserService userService; @RequestMapping(value = "/register", method= RequestMethod.POST) @ApiImplicitParam(name = "params", value = "用户信息", dataType = "RegisterUserInputDTO", paramType = "body") @ApiOperation("注册会员") public ResponseVO registerUser(@RequestBody RegisterUserInputDTO params) { // 密码加密 String passwordEncoder = passwordEncoderBCrypt.encode(params.getPassword()); return ResponseVO.ok(passwordEncoder); } @RequestMapping(value = "/login", method = RequestMethod.POST) @ApiImplicitParam(name = "params", value = "用户信息", dataType = "LoginInputDTO", paramType = "body") @ApiOperation("登录测试") public ResponseVO loginUser(@RequestBody LoginInputDTO params) { LoginOutputDTO loginOutputDTO = userService.login(params); String pwdInDb = loginOutputDTO.getPassword(); String username = params.getUsername(); String password = params.getPassword(); // 密码校对 Boolean passwordFlag = passwordEncoderBCrypt.matches(password, pwdInDb); if(passwordFlag) { return ResponseVO.ok(); }else{ return ResponseVO.empty(); } } }

    图5.1 注册

    图5.2 登录

    5.3 security密码加密说明

    security只支持单向的密码加密功能,不支持密码单独解密 即同一个明码字符串,加密后的密码不同密码校对通过matches方法,参数为(明码, 密码)

    6 小结

    shiro登录一次,其他需要鉴权的接口均可直接访问shiro加密的密码校验,需要自定义实现security可直接生成加密密码,并支持自校验security不支持单独解密

    【参考文献】 [1]https://www.jianshu.com/p/7f724bec3dc3 [2]https://www.imooc.com/qadetail/299768?lastmedia=1 [3]https://blog.csdn.net/bicheng4769/article/details/86668209 [4]https://blog.csdn.net/taojin12/article/details/88343990 [5]https://segmentfault.com/a/1190000019440231 [6]https://blog.csdn.net/afsvsv/article/details/86639482 配置swagger显示环境dev,test []https://blog.csdn.net/SSHH_ZHU/article/details/104286169 []https://www.cnblogs.com/jjsir/p/13613437.html []https://blog.csdn.net/qq_21537671/article/details/107280447 []https://www.jb51.net/article/152130.htm

    Processed: 0.013, SQL: 9