文章目录
- 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");
登录接口
- 用 LoginReqVo 接收用户提交过来的用户名密码的数据
- 把 vo 传入业务层接口进行业务处理
登录业务处理
- 通过用户名去db查询用户信息
- 判断是否查询到用信息
- 判断是否被禁用
- 校验密码是否正确
- 生成token、把token做为key、userId作为value存入redis并设置过期时间为60分钟(后面认证需要)
- 封装返回数据LoginRespVo 返回前端
获取用户详情接口
这个接口有两个关键点,因为用户首次登录后,后续再访问我们的系统资源的时候,无需再传入用户密码进行验证只需要携带登录生 成的token可以了,我们的后端会围绕token使用shiro进行一系列的认证。当用户通过了用户认证的时候还需要进行授权,因为用户详解接口设置了访问权限(@RequiresPermissions(“sys:user:detail”))所以我们还要对访问的用户进行授权。