权限管理系统——开发日记

技术栈 —— Spring + SpringMVC + Mybatis +(AdminLTE模板工具+jsp、Oracle)

一、环境搭建及产品订单操作

技术难点:

1、项目环境搭建

2、页面上、实体类、数据库中的关于时间类型字段的处理

解决:

  • 页面上接收后传递到后端的都是字符串类型,Oracle数据库中相关字段为timestamp 类型,实体类中对应时间字段应定义成如下形式来起到中转的作用
public Class Product{
	...
	@DateTimeFormat(pattern="yyyy-MM-dd HH:mm") //将从页面上接收的字符串类型数据转换为Date类型
	private Date departureTime;	
	private String departureTimeStr;	//存放上面时间的字符串形式
	
	/*将Date类型数据转换为字符串类型,并封装在相应Str属性上发送给页面*/
	public String getDepartureTimeStr() {
        if(departureTime!=null){
            departureTimeStr= DateUtils.date2String(departureTime,"yyyy-MM-dd HH:mm:ss");
       }
       return departureTimeStr;
   }
	...
}
//===========================================================================
/*自定义的工具类*/
public class DateUtils {

    //日期转换成字符串
    public static String date2String(Date date, String patt) {
        SimpleDateFormat sdf = new SimpleDateFormat(patt);
        String format = sdf.format(date);
        return format;
    }

    //字符串转换成日期
    public static Date string2Date(String str, String patt) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat(patt);
        Date parse = sdf.parse(str);
        return parse;
    }
}

3、在Service层实现分页查询

  • 使用的是PageHelper插件
  • 准备工作:导入依赖、在applicationContext.xml配置相关信息、在查询语句上开启分页
    在这里插入图片描述

配置文件中的<prop key="reasonable">true</prop>为分页合理化配置,开启则不会产生越界行为(例如在当前处于最后一页的情况下,点击下一页不会继续跳转,也不会报出异常)。
原理就是当请求页数大于最大页数时,将直接当做请求最后一页处理;同样的,当请求页数<1时,将当做请求第一页处理。

  • 跳转到展示分页数据的页面(要带上pagesize参数,并展示第一页)
<li id="system-setting"><a
	href="${pageContext.request.contextPath}/orders/findAll.do?page=1&size=4"> 
	<i class="fa fa-circle-o"></i> 订单管理
</a></li>
  • 翻页按钮上的处理
<li>
  <a href="${pageContext.request.contextPath}/orders/findAll.do?page=1&size=${pageInfo.pageSize}" aria-label="Previous">首页</a>
</li>
<li>
  <a href="${pageContext.request.contextPath}/orders/findAll.do?page=${pageInfo.pageNum-1}&size=${pageInfo.pageSize}">上一页</a></li>
<c:forEach begin="1" end="${pageInfo.pages}" var="pageNum">
  <li>
    <a href="${pageContext.request.contextPath}/orders/findAll.do?page=${pageNum}&size=${pageInfo.pageSize}">${pageNum}</a>
  </li>
</c:forEach>
<li>
  <a href="${pageContext.request.contextPath}/orders/findAll.do?page=${pageInfo.pageNum+1}&size=${pageInfo.pageSize}">下一页</a>
</li>
<li>
  <a href="${pageContext.request.contextPath}/orders/findAll.do?page=${pageInfo.pages}&size=${pageInfo.pageSize}" aria-label="Next">尾页</a>
</li>
  • Controller层处理
@Controller
@RequestMapping("/orders")
public class OrdersController {

    @Autowired
    private IOrdersService ordersService;
    
    @RequestMapping("/findAll.do")
    public ModelAndView findAll(@RequestParam(name = "page", required = true, defaultValue = "1") int page, 
    							@RequestParam(name = "size", required = true, defaultValue = "4") int size) throws Exception {
        ModelAndView mv = new ModelAndView();
        List<Orders> ordersList = ordersService.findAll(page, size);
        //PageInfo就是一个分页Bean
        PageInfo pageInfo=new PageInfo(ordersList);
        mv.addObject("pageInfo",pageInfo);
        mv.setViewName("orders-page-list");
        return mv;
    }
}
  • Service层调用
public List<Orders> findAll(int page, int size) throws Exception {

    //参数pageNum 是页码值   参数pageSize 代表是每页显示条数
    PageHelper.startPage(page, size);
    return ordersDao.findAll();
}
  • Dao层实现
@Select("select * from orders")
@Results({
        @Result(id = true, property = "id", column = "id"),
        @Result(property = "orderNum", column = "orderNum"),
        @Result(property = "orderTime", column = "orderTime"),
        @Result(property = "orderStatus", column = "orderStatus"),
        @Result(property = "peopleCount", column = "peopleCount"),
        @Result(property = "peopleCount", column = "peopleCount"),
        @Result(property = "payType", column = "payType"),
        @Result(property = "orderDesc", column = "orderDesc"),
        @Result(property = "product", column = "productId", javaType = Product.class, one = @One(select = "com.itheima.ssm.dao.IProductDao.findById")),
})
public List<Orders> findAll() throws Exception;

4、多表查询

  • 三表之间的关系:
    在这里插入图片描述
  • Dao层的设计
public interface IOrdersDao {
    //多表操作
    @Select("select * from orders where id=#{ordersId}")
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "orderNum", column = "orderNum"),
            @Result(property = "orderTime", column = "orderTime"),
            @Result(property = "orderStatus", column = "orderStatus"),
            @Result(property = "peopleCount", column = "peopleCount"),
            @Result(property = "peopleCount", column = "peopleCount"),
            @Result(property = "payType", column = "payType"),
            @Result(property = "orderDesc", column = "orderDesc"),
            @Result(property = "product", column = "productId", javaType = Product.class, one = @One(select = "com.itheima.ssm.dao.IProductDao.findById")),
            @Result(property = "travellers",column = "id",javaType =java.util.List.class,many = @Many(select = "com.itheima.ssm.dao.ITravellerDao.findByOrdersId"))
    })
    public Orders findById(String ordersId) throws Exception;
}
public interface IProductDao {
    //根据id查询产品
    @Select("select * from product where id=#{id}")
    public Product findById(String id) throws Exception;
}
public interface ITravellerDao {

    @Select("select * from traveller where id in (select travellerId from order_traveller where orderId=#{ordersId})")
    public List<Traveller> findByOrdersId(String ordersId) throws Exception;
}
  • Service层调用
@Override
public Orders findById(String ordersId) throws Exception{
    return ordersDao.findById(ordersId);
}
  • Controller层的处理
@RequestMapping("/findById.do")
public ModelAndView findById(@RequestParam(name = "id", required = true) String ordersId) throws Exception {
	ModelAndView mv = new ModelAndView();
	Orders orders = ordersService.findById(ordersId);
	mv.addObject("orders",orders);
	mv.setViewName("orders-show");
	return mv;
}


二、springSecurity框架

技术难点:

1、springSecurity的登入认证和授权

  • 它是 Spring 项目组中用来提供安全认证服务的框架。
  • 如何在项目中使用它
    在这里插入图片描述
  1. spring-security.xml配置文件
<!-- 配置不拦截的资源 -->
<security:http pattern="/login.jsp" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<!--配置具体的规则
	auto-config="true"	不用自己编写登录的页面,框架提供默认登录页面
	use-expressions="false"	是否使用SPEL表达式(没学习过)
-->
<security:http auto-config="true" use-expressions="false">

<!-- 配置具体的拦截的规则 pattern="请求路径的规则" 
						access="访问系统的人,必须有ROLE_USER、ROLE_ADMIN的角色" 
-->
<security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN"/>

<!-- 定义跳转的具体的页面 (接收参数名称默认就是:username/password)-->
 <security:form-login														//
         login-page="/login.jsp"
         login-processing-url="/login.do"	//登入表单的发送路径(不是自己定义的Controller)
         default-target-url="/index.jsp"	 //账号密码正确但无权限则进入该页面
         authentication-failure-url="/failer.jsp"
         authentication-success-forward-url="/pages/main.jsp" />
  1. web.xml中配置的filter-name不能修改(源码中用会直接引用这个名字)。
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
  1. 自定义UserService
/*自定义UserService接口必须实现 UserDetailsService */
public interface IUserService extends UserDetailsService {
}
//=========================================================

@Service("userService")
@Transactional
public class UserServiceImpl implements IUserService {

    @Autowired
    private IUserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = null;
        try {
            userInfo = userDao.findByUsername(username);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //处理自己的用户对象封装成UserDetails(User是它的实现类,springSecurity自带的)
        User user = new User(userInfo.getUsername(), "{noop}" + userInfo.getPassword(), 
        					userInfo.getStatus() == 0 ? false : true, true, true, true, 
        					getAuthority(userInfo.getRoles()));
        return user;
    }

    //作用就是返回一个List集合,集合中装入的是角色描述
    public List<SimpleGrantedAuthority> getAuthority(List<Role> roles) {

        List<SimpleGrantedAuthority> list = new ArrayList<>();
        for (Role role : roles) {
            list.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
        }
        return list;
    }
}

这里补充一点,关于上述代码中User参数里的"{noop}" + userInfo.getPassword(),

  • 因为此时的数据时我手动添加到数据库中的,密码为明文,没有用到SpringSecurity的加密技术,所以前面要加上{noop}来强调密码无加密。
  • 后续我们说到BCryptPasswordEncoder加密时,这里的密码参数前面就不用加上{noop}了。
  1. 下面是SpringSecurity框架中自带的User类部分代码
public class User implements UserDetails, CredentialsContainer { 
	private String password; 
	private final String username; 
	private final Set<GrantedAuthority> authorities; 
	private final boolean accountNonExpired; //帐户是否过期 
	private final boolean accountNonLocked; //帐户是否锁定 
	private final boolean credentialsNonExpired; //认证是否过期 
	private final boolean enabled; //帐户是否可用 


2、BCryptPasswordEncoder加密

  • 说明:对于同一密码每次加密结果都不同。下面来验证一下
public class BCryptPasswordEncoderUtils {
    private static BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
    public static String encodePassword(String password){
        return bCryptPasswordEncoder.encode(password);
    }

    public static void main(String[] args) {
        String password="123";
        String pwd = encodePassword(password);
        System.out.print(pwd.length());
    }
}
  1. 两次的运行结果分别是:
    $2a$10$tJHudmJh6MRPdiL7mv0yfe0nZJbDHuhl7sSTnqNC4DauMik9ppi4K
    $2a$10$Ce8LB3jdYDZ2f6HB281zA.4eC7v6ziJdK8MMWg0Yu8ETMg5ToMpIe
  1. 原理:
    BCryptPasswordEncoder使用哈希算法+随机盐来对字符串加密。因为哈希是一种不可逆算法,所以密码认证时需要使用相同的算法+盐值(相同的加密策略)来对待校验的明文进行加密,然后比较这两个密文来进行验证。
  • 如何在项目中使用该加密方式呢?
  1. 在spring-security.xml中配置加密类以及加密方式
<!-- 切换成数据库中的用户名和密码 -->
<security:authentication-manager>
    <security:authentication-provider user-service-ref="userService">
        <!-- 配置加密的方式-->
        <security:password-encoder ref="passwordEncoder"/>
    </security:authentication-provider>
</security:authentication-manager>

<!-- 配置加密类 -->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
  1. 在账号的保存操作上多加一步加密处理
@Service("userService")
@Transactional
public class UserServiceImpl implements IUserService {
    @Autowired
    private IUserDao userDao;
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    ...
	@Override
    public void save(UserInfo userInfo) throws Exception {
        //对密码进行加密处理
        userInfo.setPassword(bCryptPasswordEncoder.encode(userInfo.getPassword()));
        userDao.save(userInfo);
    }
	...

这里的@Service("userService")的参数名称要和spring-security.xml配置文件中相同



三、用户/角色/权限之间的关联操作

技术要点:

1、对于相关联操作的思想

  • 由于本次项目实际需求,这里本没有使用到Mybatis中的级联CUD;而是直接去操作中间表来实现对象多对多关系的更新。

2、Mybatis中insert的注意事项

  • 先看看项目中的相关代码
@Insert("insert into users_role(userId,roleId) values(#{userId},#{roleId})")
void addRoleToUser(@Param("userId") String userId, @Param("roleId") String roleId);
  • @Param注解

当我们向sql语句中插入的参数大于1个时,就必须得用@Param("数据库中的字段名称")注解来将我们自定义的参数与数据库中的相关字段给对应上,



四、服务器端方法级权限控制

技术难点:(以下三种方式都是默认关闭的)

1、JSR-250注解

  • 基本用法:
    在这里插入图片描述
  • 对上述基本用法的补充说明:
    @RolesAllowed表示访问对应方法时所应该具有的角色
	示例: @RolesAllowed({"USER", "ADMIN"}) 
			该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。
			这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN

@PermitAll表示允许所有的角色进行访问,也就是说不进行权限控制
@DenyAll是和PermitAll相反的,表示无论什么角色都不能访问

  • 若权限不符合注解规定,则会在客户端上显示出403的错误信息,403的错误类型就是权限不足。
  • 解决办法:

web.xml文件中配置相应的错误页面:

   <error-page>
       <error-code>403</error-code>
       <location>/403.jsp</location>
   </error-page>

2、@Secured注解(SpringSecurity自带的)

  • 基本用法
    在这里插入图片描述

  • 产生的效果以及对于权限不足的解决办法同JSR-250注解相同。

3、支持表达式的注解(SpEL表达式)

  • 基本用法
    在这里插入图片描述

  • 对上述基与表达式注解的补充说明:

	支持表达式的注解在使用上会比前两种注解更加灵活,功能更加强大;
	但同样的与其对于的学习成本也更高。
	
下面是比较复杂的一个示例:
	@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)") 
	void changePassword(@P("userId") long userId ){ ... } 
	
//这里表示在changePassword方法执行之前,
//先判断方法参数userId的值是否等于principal中保存的当前用户的 userId,
//或者当前用户是否具有ROLE_ADMIN权限,两种符合其一,就可以访问该方法。
  • 产生的效果以及对于权限不足的解决办法同JSR-250注解相同。


五、页面端标签控制权限

技术难点:

1、authentication/authorize的使用

  • 基本用法
    在这里插入图片描述
  • 对于上述security:authorize用法的补充

authorize是用来判断普通权限的,通过判断用户是否具有对应的权限而控制其所包含内容的显示:

<security:authorize access="" method="" url="" var=""></security:authorize>
  • access: 需要使用表达式来判断权限,当表达式的返回结果为true时表示拥有对应的权限
  • method:method属性是配合url属性一起使用的,表示用户应当具有指定url指定method访问的权限,
  • method的默认值为GET,可选值为http请求的7种方法
  • url:url表示如果用户拥有访问指定url的权限即表示可以显示authorize标签包含的内容
  • var:用于指定将权限鉴定的结果存放在pageContext的哪个属性中


六、AOP日志

技术难点:(JoinPoint)

1、JoinPoint

2、获取访问的类和方法

  • 方法要分有参无参
@Component
@Aspect
public class LogAop {
    @Autowired
    private HttpServletRequest request;
    @Autowired
    private ISysLogService sysLogService;

    private Date visitTime; //开始时间
    private Class clazz; //访问的类
    private Method method;//访问的方法
	//前置通知  主要是获取开始时间,执行的类是哪一个,执行的是哪一个方法
	@Before("execution(* com.itheima.ssm.controller.*.*(..))")
	public void doBefore(JoinPoint jp) throws NoSuchMethodException {
	    
	    visitTime = new Date();//当前时间就是开始访问的时间
	    
	    clazz = jp.getTarget().getClass(); //具体要访问的类
	    String methodName = jp.getSignature().getName(); //获取访问的方法的名称
	    Object[] args = jp.getArgs();//获取访问的方法的参数
	
	    //获取具体执行的方法的Method对象
	    if (args == null || args.length == 0) {
	        method = clazz.getMethod(methodName); //只能获取无参数的方法
	    } else {
	        Class[] classArgs = new Class[args.length];
	        for (int i = 0; i < args.length; i++) {
	            classArgs[i] = args[i].getClass();
	        }
	        method = clazz.getMethod(methodName, classArgs);
	    }
	}
	......

3、获取访问的url和访问的时长

  • 访问时长 = @After执行时间 - @Before执行时间
  • url = 类上的@RequestMapping("/父路径")+ 方法上的@RequestMapping("/子路径")
//后置通知
@After("execution(* com.itheima.ssm.controller.*.*(..))")
public void doAfter(JoinPoint jp) throws Exception {
    long time = new Date().getTime() - visitTime.getTime(); //获取访问的时长

    String url = "";
    //获取url
    if (clazz != null && method != null && clazz != LogAop.class) {
        //1.获取类上的@RequestMapping("/orders")
        RequestMapping classAnnotation = (RequestMapping) clazz.getAnnotation(RequestMapping.class);
        if (classAnnotation != null) {
            String[] classValue = classAnnotation.value();
            //2.获取方法上的@RequestMapping("/xxx")
            RequestMapping methodAnnotation = method.getAnnotation(RequestMapping.class);
            if (methodAnnotation != null) {
                String[] methodValue = methodAnnotation.value();
                url = classValue[0] + methodValue[0];
...

4、获取访问的ip和当前操作的用户

  • HttpServletRequestgetRemoteAddr()方法获取ip
  • 通过SecurityContextHolder.getContext()方法获取当前用户

// 也可以从request.getSession中获取
request.getSession().getAttribute(“SPRING_SECURITY_CONTEXT”)

//获取访问的ip
String ip = request.getRemoteAddr();

//获取当前操作的用户
SecurityContext context = SecurityContextHolder.getContext();//从上下文中获了当前登录的用户
User user = (User) context.getAuthentication().getPrincipal();
String username = user.getUsername();
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值