文章目录

  • 1. 配置Swagger
  • 2. 注入RedisTemplate
  • 3. 配置redis连接池
  • 4. 自定义运行时异常
  • 5. 数据库表
  • 6. 自定义 AccessControlFilter token校验
  • 7. 自定义 Realm
  • 8. shiro核心配置
  • 9. 实现用户登录认证访问授权


1. 配置Swagger

@EnableSwagger2
@Configuration
public class SwaggerConfig {

    @Value("${swagger2.enable}")
    private boolean enable;

    @Bean
    public Docket createRestApi() {
        /**
         * 这是为了我们在用 swagger 测试接口的时候添加头部信息
         */
        List<Parameter> pars = new ArrayList<Parameter>();
        ParameterBuilder tokenPar = new ParameterBuilder();
        tokenPar.name("sessionId").description("swagger测试用(模拟sessionId传入)非必填 header")
                .modelRef(new ModelRef("string"))
                .parameterType("header")
                .required(false);
                pars.add(tokenPar.build());

        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.test.shiro.controller"))
                .paths(PathSelectors.any())
                .build()
                .globalOperationParameters(pars)
                .enable(enable);
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("茶花--shiro 实战")
                .description("茶花-spring boot 实战系列")
                .termsOfServiceUrl("")
                .version("1.0")
                .build();
    }
}

2. 注入RedisTemplate

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object>  redisTemplate(RedisConnectionFactory  redisConnectionFactory){
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        //设置key的序列化方式
        template.setKeySerializer(RedisSerializer.string());
        // 设置value的序列化方式
        template.setValueSerializer(RedisSerializer.json());
        template.setHashKeySerializer(RedisSerializer.string());
        template.setHashValueSerializer(RedisSerializer.json());
        template.afterPropertiesSet();
        return template;
    }
}

3. 配置redis连接池

# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务?连接端?
spring.redis.port=6379
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=100
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=PT10S
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=30
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=1
#链接超时时间
spring.redis.timeout=PT10S

4. 自定义运行时异常

public class BusinessException extends RuntimeException {
    private String code;
    private String message;
    
    public BusinessException(String code,String message){
        super(message);
        this.code = code;
        this.message = message;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public String getCode() {
        return code;
    }
}

5. 数据库表

DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
  `id` varchar(64) NOT NULL COMMENT '用户id',
  `username` varchar(50) NOT NULL COMMENT '账户名称',
  `salt` varchar(20) DEFAULT NULL COMMENT '加密盐值',
  `password` varchar(200) NOT NULL COMMENT '用户密码密文',
  `phone` varchar(11) DEFAULT NULL COMMENT '手机号码',
  `dept_id` varchar(64) DEFAULT NULL COMMENT '部门id',
  `real_name` varchar(60) DEFAULT NULL COMMENT '真实名称',
  `nick_name` varchar(60) DEFAULT NULL COMMENT '昵称',
  `email` varchar(50) DEFAULT NULL COMMENT '邮箱(唯一)',
  `status` tinyint(4) DEFAULT '1' COMMENT '账户状态(1.正常 2.锁定)',
  `sex` tinyint(4) DEFAULT NULL COMMENT '性别(1.男 2.女)',
  `deleted` tinyint(4) DEFAULT '0' COMMENT '是否删除(0未删除;1已删除)',
  `create_id` varchar(64) DEFAULT NULL COMMENT '创建人',
  `update_id` varchar(64) DEFAULT NULL COMMENT '更新人',
  `create_where` tinyint(4) DEFAULT NULL COMMENT '创建来源(1.web 2.android 3.ios )',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('11c58573-6972-4de1-ae46-8965bc3c4a5d', 'dev123', 'ac92e98504c14fd19060', '8583ab5a0d7804e23bfa55dce5436e74', '13878828969', null, null, null, '163@.com', '1', null, '0', null, null, null, '2020-01-02 19:25:38', null);
INSERT INTO `sys_user` VALUES ('3daac6c5-e657-4e95-b665-c1fa26d9697a', 'dev', '07d6996c521747739697', '0430c7bd4b49770046171e163656ebdd', '13878828996', null, null, null, '163@.com', '1', null, '0', null, null, null, '2020-01-02 19:01:51', '2020-01-02 19:20:59');
INSERT INTO `sys_user` VALUES ('7278351d-e411-4e7a-a270-1514c6a6c0ef', 'dev1111', 'c5299cad69354100b251', '3eaadb869b6f5ec477a6db33b651b87d', '13878569982', null, null, null, null, '1', null, '0', null, null, null, '2020-01-02 23:19:34', null);
INSERT INTO `sys_user` VALUES ('9a26f5f1-cbd2-473d-82db-1d6dcf4598f8', 'admin', '324ce32d86224b00a02b', 'ac7e435db19997a46e3b390e69cb148b', '13888888888', '24f41c71-5a95-4ef4-9493-174574f3b0c5', null, null, 'yingxue@163.com', '1', null, '0', null, null, '3', '2019-09-22 19:38:05', null);

通过easycode插件自动生成dao,service,controller,mapper代码

6. 自定义 AccessControlFilter token校验

① 自定义 token

public class CustomPasswordToken  extends UsernamePasswordToken {
    
    private String token;
    
    public CustomPasswordToken(String token){
        this.token = token;
    }

    /**
     * 重写父类的方法
     */
    @Override
    public Object getPrincipal(){
        return token;
    }
    
}

② 自定义 AccessControlFilter 用户凭证校验

@Slf4j
public class CustomAccessControlFilter extends AccessControlFilter {

    /**
     * 是否允许访问
     * true:放行,交给下一个Filter处理
     * false:往下执行onAccessDenied()方法
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {

        return false;
    }

    /**
     * 表示访问拒绝时是否自己处理,
     * 如果返回true表示自己不处理且继续拦截器链执行,
     * 返回false表示自己已经处理了(比如重定向到另一个页面)。
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException, ServletException {
        HttpServletRequest request= (HttpServletRequest) servletRequest;
        try {
            //获取用户凭证
            String accessToken=request.getHeader("sessionId");
            if(StringUtils.isEmpty(accessToken)){
                throw new BusinessException(4001002,"用户凭证为空请重新登录");
            }
            CustomPasswordToken customPasswordToken = new CustomPasswordToken(accessToken);
            // 委托给Realm进行登录
            getSubject(servletRequest, servletResponse).login(customPasswordToken);
        }catch (BusinessException e) {
            customRsponse(e.getCode(),e.getMessage(),servletResponse);
            return false;
        } catch (AuthenticationException e) {
            if(e.getCause() instanceof BusinessException){
                BusinessException exception= (BusinessException) e.getCause();
                customRsponse(exception.getCode(),exception.getMessage(),servletResponse);
                return false;
            }else {
                customRsponse(4000001,"用户认证失败",servletResponse);
                return false;
            }
        }catch (AuthorizationException e){
            if(e.getCause() instanceof BusinessException){
                BusinessException exception= (BusinessException) e.getCause();
                customRsponse(exception.getCode(),exception.getMessage(),servletResponse);
                return false;
            }else {
                customRsponse(4030001,"没有访问的权限",servletResponse);
                return false;
            }
        }
        catch (Exception e){
            if(e.getCause() instanceof BusinessException){
                BusinessException exception= (BusinessException) e.getCause();
                customRsponse(exception.getCode(),exception.getMessage(),servletResponse);
                return false;
            }else {
                customRsponse(5000001,"系统异常",servletResponse);
                return false;
            }
        }
        return true;
    }

    public void customRsponse(int code, String msg, ServletResponse response){
        // 自定义异常的类,用户返回给客户端相应的JSON格式的信息
        try {
            Map<String,Object> result=new HashMap<>();
            result.put("code",code);
            result.put("msg",msg);
            response.setContentType("application/json; charset=utf-8");
            response.setCharacterEncoding("UTF-8");
            String userJson = JSON.toJSONString(result);
            OutputStream out = response.getOutputStream();
            out.write(userJson.getBytes("UTF-8"));
            out.flush();
        } catch (IOException e) {
            log.error("eror={}",e);
        }
    }
}

③ 自定义 CredentialsMatcher

public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        String sessionId = (String)((CustomPasswordToken) token).getPrincipal();
        if(!redisTemplate.hasKey(sessionId)){
            throw new BusinessException(4001002,"授权信息信息无效请重新登录");
        }
        return true;
    }
}

7. 自定义 Realm

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private RedisTemplate redisTemplate;
    
    @Override
    public boolean supports(AuthenticationToken token) {
        return  token instanceof CustomPasswordToken;
    }

    /**
     * 主要业务:
     * 系统业务出现要验证用户的角色权限的时候,就会调用这个方法来获取该用户所拥有的角色/权限
     */

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
        String token = (String) SecurityUtils.getSubject().getPrincipal();
        String userId = (String) redisTemplate.opsForValue().get(token);
        authorizationInfo.addRoles(getRolesByUserId(userId));
        authorizationInfo.setStringPermissions(getPermissionByUserId(userId));
        return authorizationInfo;
    }

    /**
     * 主要业务:
     * 当业务代码调用 subject.login(customPasswordToken); 方法后,就会自动调用这个方法,验证用户名/密码
     * 这里我们改造成验证token是否有效
     */

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        CustomPasswordToken token= (CustomPasswordToken) authenticationToken;
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token.getPrincipal(), token.getPrincipal(), getName());
        return simpleAuthenticationInfo;
    }

    /**
     * 获取用户的角色
     */
    private List<String> getRolesByUserId(String userId) {
        List<String> roles = new ArrayList<>();
        if(userId.equals("9a26f5f1-cbd2-473d-82db-1d6dcf4598f8")){
            roles.add("admin");
        }else {
            roles.add("test");
        }
        return roles;
    }
    /**
     * 获取用户的权限
     */
    private Set<String> getPermissionByUserId(String userId) {
        Set<String> permissions = new HashSet<>();
        /**
         * 只有是 admin 用户才拥有所有权限
         */
        if(userId.equals("9a26f5f1-cbd2-473d-82db-1d6dcf4598f8")){
            permissions.add("*");
        }else {
            permissions.add("sys:user:edit");
            permissions.add("sys:user:list");
        }
        return permissions;
    }

}

8. shiro核心配置

@Configuration
public class ShiroConfig {

    /**
     * 自定义密码校验
     */
    @Bean
    public CustomHashedCredentialsMatcher customHashedCredentialsMatcher(){
        return new CustomHashedCredentialsMatcher();
    }

    /**
     * 自定义域
     */
    @Bean
    public CustomRealm customRealm(){
        CustomRealm customRealm = new CustomRealm();
        customRealm.setCredentialsMatcher(customHashedCredentialsMatcher());
        return customRealm;
    }

    /**
     * 安全管理
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        securityManager.setRealm(customRealm());
        return securityManager;
    }

    /**
     * shiro过滤器,配置拦截哪些请求
     */
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //自定义拦截器限制并发人数,参考博客:
        LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        //用来校验token
        filtersMap.put("token", new CustomAccessControlFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/sysUser/user/login", "anon");
        //放开swagger-ui地址
        filterChainDefinitionMap.put("/swagger/**", "anon");
        filterChainDefinitionMap.put("/v2/api-docs", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/csrf","anon");
        filterChainDefinitionMap.put("/**","token,authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 开启shiro aop注解
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager
                                                                                           securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new
                AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new
                DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
}

9. 实现用户登录认证访问授权

@Service("sysUserService")
public class SysUserServiceImpl implements SysUserService {

    @Resource
    private SysUserDao sysUserDao;
    
    @Autowired
    private RedisTemplate redisTemplate;

    public LoginRespVo login(LoginReqVo reqVo){
        SysUser userByName =  sysUserDao.queryByName(reqVo.getUsername());
        if (userByName == null) {
            throw new BusinessException(4001004, "用户名密码不匹配");
        }
        if (userByName.getStatus() == 2) {
            throw new BusinessException(4001004, "该账户已经被锁定,请联系系统管理员");
        }
        if(!getPasswordMatcher(reqVo.getPasswoed(),userByName.getSalt()).equals(userByName.getPassword())){
            throw new BusinessException(4001004, "用户名密码不匹配");
        }
        LoginRespVo loginRespVo = new LoginRespVo();
        loginRespVo.setId(userByName.getId());
        String token = UUID.randomUUID().toString();
        loginRespVo.setToken(token);
        redisTemplate.opsForValue().set(token,userByName.getId(),60,TimeUnit.MINUTES);
        return loginRespVo;
    }

    /**
     * 获取密文密码
     * @param currentPassword
     * @param salt
     * @return
     */
    private String getPasswordMatcher(String currentPassword,String salt){
        return new Md5Hash(currentPassword, salt).toString();
    }
    
    public SysUser detail(String id) {
        return sysUserDao.queryById(id);
    }
}
@RestController
@RequestMapping("sysUser")
public class SysUserController {
    /**
     * 服务对象
     */
    @Resource
    private SysUserService sysUserService;

    @PostMapping("/user/login")
    @ApiOperation(value ="用户登录接口")
    public Map<String, Object> login(@RequestBody LoginReqVo vo){
        Map<String,Object> result=new HashMap<>();
        result.put("code",0);
        result.put("data",sysUserService.login(vo));
        return result;
    }

    @GetMapping("/user/{id}")
    @ApiModelProperty(value = "查询用户详情接口")
    @RequiresPermissions("sys:user:detail")
    public Map<String, Object> detail(@PathVariable("id") @ApiParam(value = "用户Id") String id){
        Map<String,Object> result=new HashMap<>();
        result.put("code",0);
        result.put("data",sysUserService.detail(id));
        return result;
    }
}

配置登录接口白名单:

filterChainDefinitionMap.put("/api/user/login","anon");

登录接口

  1. 用 LoginReqVo 接收用户提交过来的用户名密码的数据
  2. 把 vo 传入业务层接口进行业务处理

登录业务处理

  1. 通过用户名去db查询用户信息
  2. 判断是否查询到用信息
  3. 判断是否被禁用
  4. 校验密码是否正确
  5. 生成token、把token做为key、userId作为value存入redis并设置过期时间为60分钟(后面认证需要)
  6. 封装返回数据LoginRespVo 返回前端

获取用户详情接口

这个接口有两个关键点,因为用户首次登录后,后续再访问我们的系统资源的时候,无需再传入用户密码进行验证只需要携带登录生 成的token可以了,我们的后端会围绕token使用shiro进行一系列的认证。当用户通过了用户认证的时候还需要进行授权,因为用户详解接口设置了访问权限(@RequiresPermissions(“sys:user:detail”))所以我们还要对访问的用户进行授权。