基于springboot和shiro的整合
准备工作,首先创建springboot项目:
1:首先防止下载慢的问题,将地址换成https://start.aliyun.com
2:一路next下去:
3:勾选对应的选项,然后在next,直接finish:
添加pom依赖:
<!--shiro的依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.2</version>
</dependency>
<!-- thymeleaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--shrio和thymeleaf集成的扩展依赖,为了能在页面上使用xsln:shrio的标签 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!--druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<!--分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.13</version>
</dependency>
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
在resources下面的创建.yml配置文件
server:
port: 8080
spring:
datasource: #数据源配置
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/shiro?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=UTC
username: root
password: 123456
druid:
max-active: 10
min-idle: 5
max-wait: 5000
initial-size: 5
validation-query: select 1
#druid监控页面配置
stat-view-servlet:
enabled: true
login-username: admin
login-password: admin
allow:
deny:
url-pattern: "/druid/*"
thymeleaf:
cache: false
#mybatis的配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations:
- classpath:mapper/ *Mapper.xml
#shiro的配置
shiro:
hash-algorithm-name: md5
hash-iterations: 2
#未登录跳转的页面
#login-url: /index.html
#未授权跳转的页面
#unauthorized-url: /unauthorized.html
#设置放行页面,这里是登录页面和去登录的页面都是放行的,不进行shiro验证
anon-urls:
- /index.html*
- /login.html*
- /login/toLogin*
- /login/login*
#配置登出后页面
logout-url: /login/logout*
#配置拦截需要验证的页面
authc-urls:
- /**
建一个filter的包,自己创建一个过滤器
若没登录就进入主页面的情况,会回调onAccessDenied方法,那我们可以重写这个方法用来提醒前端。
public class ShiroLoginFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//if (isAjax(request)) {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
Map<String,Object> resultData = new HashMap<>();
resultData.put("code", -1);
resultData.put("msg", "未登录!");
httpServletResponse.getWriter().write(JSONObject.toJSON(resultData).toString());
/* } else {
// saveRequestAndRedirectToLogin(request, response);
*//**
* @Mark 非ajax请求重定向为登录页面
*//*
httpServletResponse.sendRedirect("/login.jsp");
}*/
return false;
}
private boolean isAjax(ServletRequest request) {
String header = ((HttpServletRequest) request).getHeader("X-Requested-With");
if ("XMLHttpRequest".equalsIgnoreCase(header)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
建一个aspect的包,写一个监控控制器异常的类AppExceptionAdivse
这个类用来监控权限,他相当于一个aop,所以我们也引进了aop的包,如果访问了没有授权的页面,则会抛出异常UnauthorizedException,因为我们没有配置未登录后跳转的页面,而是返回一个json来通知前段,这样做到了前后端的分离。
@RestControllerAdvice //以json串的形式返回出去,全局的异常监控,类似于切面
public class AppExceptionAdivse {
//监控是否抛出未授权的异常UnauthorizedException
@ExceptionHandler(value= {UnauthorizedException.class})
public Map<String, Object> unauthorized() {
Map<String, Object> map=new HashMap<>();
map.put("code", 302);
map.put("msg", "未授权");
System.out.println("未授权");
return map;
}
}
建一个名叫config的包用来写shiro的配置类
对于以上由于springboot没有web.xml文件和.xml配置文件,我们可以用配置类的方式来写。
1:首先写第一个类ShiroProperties,把.yml的shiro配置引入。
@ConfigurationProperties(prefix = "shiro") //注入.yml文件前缀是shiro的属性
@Data //给该类get和set方法
public class ShiroProperties {
private String hashAlgorithmName="md5";
private Integer hashIterations=2;
private String loginUrl;
private String unauthorizedUrl;
private String [] anonUrls;
private String logoutUrl;
private String [] authcUrls;
}
2.在同一个包下写一个配置类ShiroAutoConfiguration,这里相当于把.xml的配置,把以上写好的数据注入到里面:
@Configuration //声明是配置类
@EnableConfigurationProperties(ShiroProperties.class) //引入ShiroProperties.class
public class ShiroAutoConfiguration {
@Autowired
private ShiroProperties shiroProperties;
/**
* 创建凭证匹配器
*/
@Bean
public HashedCredentialsMatcher credentialsMatcher(){
HashedCredentialsMatcher credentialsMatcher=new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(shiroProperties.getHashAlgorithmName());
credentialsMatcher.setHashIterations(shiroProperties.getHashIterations());
return credentialsMatcher;
}
/**
* 创建realm
*/
@Bean
public UserRealm userRealm(CredentialsMatcher credentialsMatcher){
UserRealm userRealm=new UserRealm();
//注入凭证匹配器
userRealm.setCredentialsMatcher(credentialsMatcher);
return userRealm;
}
/**
* 声明安全管理器
*/
@Bean("securityManager")
public SecurityManager securityManager(UserRealm userRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 配置过滤器 Shiro 的Web过滤器 id必须和web.xml里面的shiroFilter的 targetBeanName的值一样
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
//注入安全管理器
bean.setSecurityManager(securityManager);
//注入登陆页面
bean.setLoginUrl(shiroProperties.getLoginUrl());
//注入未授权的页面地址
bean.setUnauthorizedUrl(shiroProperties.getUnauthorizedUrl());
//注入过滤器
Map<String, String> filterChainDefinition=new HashMap<>();
//注入放行地址
if(shiroProperties.getAnonUrls()!=null&&shiroProperties.getAnonUrls().length>0){
String[] anonUrls = shiroProperties.getAnonUrls();
for (String anonUrl : anonUrls) {
filterChainDefinition.put(anonUrl,"anon");
}
}
//注入登出的地址
if(shiroProperties.getLogoutUrl()!=null){
filterChainDefinition.put(shiroProperties.getLogoutUrl(),"logout");
}
//注拦截的地址
String[] authcUrls = shiroProperties.getAuthcUrls();
if(authcUrls!=null&&authcUrls.length>0){
for (String authcUrl : authcUrls) {
filterChainDefinition.put(authcUrl,"authc");
}
}
bean.setFilterChainDefinitionMap(filterChainDefinition);
//创建自定义filter,这个就是上面自定义的过滤器,ShiroLoginFilter用来判断权限
ShiroLoginFilter filter=new ShiroLoginFilter() ;
Map<String, Filter> map=new HashMap<>( ) ;
map. put ("authc", filter) ;
bean. setFilters(map) ;
return bean;
}
//以下相当于在web.xml的配置
/**
* 注册过滤器
*/
@Bean
public FilterRegistrationBean<DelegatingFilterProxy> filterRegistrationBeanDelegatingFilterProxy(){
FilterRegistrationBean<DelegatingFilterProxy> bean=new FilterRegistrationBean<>();
//创建过滤器,注入上面出现的bean,名字保持一样
DelegatingFilterProxy proxy=new DelegatingFilterProxy();
bean.setFilter(proxy);
bean.addInitParameter("targetFilterLifecycle","true");
bean.addInitParameter("targetBeanName","shiroFilter");
// bean.addUrlPatterns();
List<String> servletNames=new ArrayList<>();
servletNames.add(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
bean.setServletNames(servletNames);
return bean;
}
/**
* 这里是为了能在html页面引用shiro标签,上面两个函数必须添加,不然会报错
*/
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
/*加入注解的使用,不加入这个注解不生效--开始*/
/**
*
* @param securityManager
* @return
*/
//自己写的异常监听器的扫描
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/*加入注解的使用,不加入这个注解不生效--结束*/
}
以上配置好之后,建一个realm包,写UserRealm类
这个类用来执行提交过来的用户名和密码的校验和权限角色的校验。
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
@Override
public String getName() {
return this.getClass().getSimpleName();
}
/**
* 作认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 得到用户名
String username = token.getPrincipal().toString();
// 根据用户名查询用户对象
User user = userService.queryUserByUserName(username);
if (null != user) {
ActiverUser activerUser = new ActiverUser();
activerUser.setUser(user);
// 根据用户名查询角色
List<String> roles = roleService.queryRoleByUserId(user.getUserid());
// 根据用户名查询权限
List<String> permissions = permissionService.queryPermissionByUserId(user.getUserid());
activerUser.setRoles(roles);
activerUser.setPermissions(permissions);
ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUsername() + user.getAddress());
/**
* 参数1传输对象 可以传到doGetAuthorizationInfo 也可以传到subject.getPrincipal() 参数2 加密后的字符串
* 参数3 盐 参数4 当前类名
*/
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(activerUser, user.getUserpwd(),
credentialsSalt, getName());
return info;
}
return null;
}
/**
* 作授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
ActiverUser activerUser = (ActiverUser) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 根据用户名查询角色
List<String> roles = activerUser.getRoles();
// 根据用户名查询权限
List<String> permissions = activerUser.getPermissions();
if (null != roles && roles.size() > 0) {
info.addRoles(roles);
}
if (null != permissions && permissions.size() > 0) {
info.addStringPermissions(permissions);
}
return info;// 如果返回空,代表当前主体没有任何角色和权限信息
}
}
因为每次匹配角色和权限时,都会调用一次doGetAuthorizationInfo方法,如果用这个方法来确认该用户的角色和权限,那么每一次都会访问一下数据库,会造成不必要的浪费,那我们可以在第一次做用户密码认证的时候将其全部查出来放在一个ActiverUser 类里面,这样就可以查一次解决下面的问题了。
建一个common的包,写ActiverUser 类:
public class ActiverUser {
private User user; //存用户对象,用户对象包含用户名密码等字段
private List<String> roles; //存角色
private List<String> permissions; //存权限
public ActiverUser() {
// TODO Auto-generated constructor stub
}
public ActiverUser(User user, List<String> roles, List<String> permissions) {
super();
this.user = user;
this.roles = roles;
this.permissions = permissions;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public List<String> getPermissions() {
return permissions;
}
public void setPermissions(List<String> permissions) {
this.permissions = permissions;
}
}
建一个controller包,写登录控制器LoginController和用户控制器UserController
LoginController:
@RestController
@RequestMapping("login")
public class LoginController {
/**
* 登陆
*/
@RequestMapping("login")
public Map<String,Object> login(String username,String password,HttpSession session) {
Map<String,Object> map=new HashMap<>();
//封装token
UsernamePasswordToken token=new UsernamePasswordToken(username, password);
//得到主体
Subject subject = SecurityUtils.getSubject();
try {
//这个方法会调用realm里面的认证来处理用户名和密码
subject.login(token);
//获取返回的user对象,若返回空的情况,shiro会抛出用户名和密码不正确的父异常AuthenticationException
ActiverUser activerUser = (ActiverUser) subject.getPrincipal();
session.setAttribute("user", activerUser.getUser());
map.put("code", 200);
map.put("msg", "登陆成功");
return map;
} catch (AuthenticationException e) {
e.printStackTrace();
map.put("code", -1);
map.put("msg", "登陆失败 用户名或密码不正确");
return map;
}
}
}
usercontroller:
@RestController
@RequestMapping("user")
public class UserController {
//要求subject中必须同时含有user:query的权限才能执行方法。否则抛出异常AuthorizationException,这里才体现了上面的监控异常处理器的作用。
@RequiresPermissions(value= {"user:query"})
@RequestMapping("query")
public Map<String,Object> query() {
Map<String,Object> map=new HashMap<>();
map.put("msg", "query");
return map;
}
@RequiresPermissions(value= {"user:add"})
@RequestMapping("add")
public Map<String,Object> add() {
Map<String,Object> map=new HashMap<>();
map.put("msg", "add");
return map;
}
@RequiresPermissions(value= {"user:update"})
@RequestMapping("update")
public Map<String,Object> update() {
Map<String,Object> map=new HashMap<>();
map.put("msg", "update");
return map;
}
@RequiresPermissions(value= {"user:delete"})
@RequestMapping("delete")
public Map<String,Object> delete() {
Map<String,Object> map=new HashMap<>();
map.put("msg", "delete");
return map;
}
@RequiresPermissions(value= {"user:export"})
@RequestMapping("export")
public Map<String,Object> export() {
Map<String,Object> map=new HashMap<>();
map.put("msg", "export");
return map;
}
}
这样整体功能整合完毕了
下面是实体类domain,和service,mapper的写法:
建一个domain的包
User:
public class User {
private Integer userid;
private String username;
private String userpwd;
private String sex;
private String address;
public Integer getUserid() {
return userid;
}
public void setUserid(Integer userid) {
this.userid = userid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
public String getUserpwd() {
return userpwd;
}
public void setUserpwd(String userpwd) {
this.userpwd = userpwd == null ? null : userpwd.trim();
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex == null ? null : sex.trim();
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address == null ? null : address.trim();
}
}
Role:
public class Role {
private Integer roleid;
private String rolename;
public Integer getRoleid() {
return roleid;
}
public void setRoleid(Integer roleid) {
this.roleid = roleid;
}
public String getRolename() {
return rolename;
}
public void setRolename(String rolename) {
this.rolename = rolename == null ? null : rolename.trim();
}
}
Permission :
public class Permission {
private Integer perid;
private String pername;
private String percode;
public Integer getPerid() {
return perid;
}
public void setPerid(Integer perid) {
this.perid = perid;
}
public String getPername() {
return pername;
}
public void setPername(String pername) {
this.pername = pername == null ? null : pername.trim();
}
public String getPercode() {
return percode;
}
public void setPercode(String percode) {
this.percode = percode == null ? null : percode.trim();
}
}
Service包
userservice:
public interface UserService {
/**
* 根据用户ID查询用户对象
*/
public User queryUserByUserName(String username);
}
userserviceimpl:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User queryUserByUserName(String username) {
return userMapper.queryUserByUserName(username);
}
}
RoleService:
public interface RoleService {
/**
* 根据用户名查询用户角色列表
*/
public List<String> queryRoleByUserId(Integer userId);
}
RoleServiceimpl:
@Service
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleMapper roleMapper;
@Override
public List<String> queryRoleByUserId(Integer userId) {
List<Role> list = roleMapper.queryRolesByUserId(userId);
List<String> roles=new ArrayList<String>();
for (Role role : list) {
roles.add(role.getRolename());
}
return roles;
}
}
PermissionService:
public interface PermissionService {
/**
* 根据用户ID查询用户权限列表
*/
public List<String> queryPermissionByUserId(Integer userId);
}
PermissionServiceimpl:
@Service
public class PermissionServiceImpl implements PermissionService {
@Autowired
private PermissionMapper permissionMapper;
@Override
public List<String> queryPermissionByUserId(Integer userId) {
List<Permission> list = permissionMapper.queryPermissionByUserId(userId);
List<String> permissions=new ArrayList<>();
for (Permission permission : list) {
permissions.add(permission.getPercode());
}
return permissions;
}
}
建立一个mapper包
UerMapper:
public interface UserMapper {
/**
* 根据用户名查询用户对象
*/
User queryUserByUserName(@Param("username") String username);
}
RoleMapper:
public interface RoleMapper {
/**
* 根据用户ID查询角色
* 1,如果只有一个参数,可以不加 怎么取?#{userId} #{saddfasdfasd}
* 2,如果有多个参数 [如何区分]
* 1,注解 @Param("userId")
* 2, #{param1} #{param2} #{1} @{2}
*/
List<Role> queryRolesByUserId(@Param("userId") Integer userId);
}
PermissionMapper:
public interface PermissionMapper {
/**
* 根据用户ID查询权限
*/
List<Permission> queryPermissionByUserId(Integer userId);
}
在resource下面建一个mapper,这是来装映射的xml
UserMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sxt.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.sxt.domain.User">
<id column="userid" jdbcType="INTEGER" property="userid" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="userpwd" jdbcType="VARCHAR" property="userpwd" />
<result column="sex" jdbcType="VARCHAR" property="sex" />
<result column="address" jdbcType="VARCHAR" property="address" />
</resultMap>
<sql id="Base_Column_List">
userid, username, userpwd, sex, address
</sql>
<!-- 根据用户名查询用户 -->
<select id="queryUserByUserName" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user
where username = #{username}
</select>
</mapper>
RoleMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sxt.mapper.RoleMapper">
<resultMap id="BaseResultMap" type="com.sxt.domain.Role">
<id column="roleid" jdbcType="INTEGER" property="roleid" />
<result column="rolename" jdbcType="VARCHAR"
property="rolename" />
</resultMap>
<!-- 根据用户ID查询角色 -->
<select id="queryRolesByUserId" resultMap="BaseResultMap">
select t1.* from role t1 inner join user_role t2 on(t1.roleid=t2.roleid)
where t2.userid=#{userId}
</select>
</mapper>
PermissionMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sxt.mapper.PermissionMapper">
<resultMap id="BaseResultMap"
type="com.sxt.domain.Permission">
<id column="perid" jdbcType="INTEGER" property="perid" />
<result column="pername" jdbcType="VARCHAR" property="pername" />
<result column="percode" jdbcType="VARCHAR" property="percode" />
</resultMap>
<!-- 根据用户ID查询权限 -->
<select id="queryPermissionByUserId" resultMap="BaseResultMap">
select distinct t1.* from permission t1 inner join role_permission t2 inner
join user_role t3
on(t1.perid=t2.perid and t2.roleid=t3.roleid) where t3.userid=#{userId}
</select>
</mapper>
最后在启动类上加注解扫描一下mapper,就可以测试了:
启动类:
@SpringBootApplication
@MapperScan(basePackages = {"com.sxt.mapper"}) //扫描mapper
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class, args);
}
}
上数据库:
user:
permission:
role:
user_role:
role_permission:
由上述关系可以看出zhangsan除了export导出权限全部拥有,我们进行测试:
登录成功测试:
http://localhost:8080/login/login?username=zhangsan&password=123456
成功后我们访问张三的权限delete:
http://localhost:8080/user/delete
访问成功
访问张三的没有的权限export:
http://localhost:8080/user/export
访问失败
登录失败
http://localhost:8080/login/login?username=zhangsan&password=12345
没有登录后访问权限页面
总结:
这里测试就全部成功了,分析一下过程,首先配置了登录会给放行不通过shiro,这样前端会给后端传过来用户名和密码,调用login方法后会调用realm包的类来进行认证,认证通过会查询数据库将所有的信息封装在ActiverUser,失败会抛出异常,返回给前端用户名密码不正确的json,如果访问该用户没有授权的页面,注解@RequiresPermissions起到作用会抛出异常,返回给前端对应的json。